onedrop_eval/evaluator/
gmegabuf.rs

1//! `gmegabuf` / `megabuf` write rewrite for the preprocess pipeline.
2//!
3//! evalexpr rejects assignments whose LHS is a function call, but MD2 presets
4//! commonly write `gmegabuf(idx) = val` and the compound forms `+=` / `-=` /
5//! `*=` / `/=`. This module rewrites them to a two-argument
6//! `gmegabuf_set(idx, val)` call shape that the math-function registry
7//! understands.
8
9use super::{is_ident_byte, match_close_paren};
10
11/// Rewrite `gmegabuf(<idx>) = <val>` (and `megabuf`) into
12/// `gmegabuf_set(<idx>, <val>)`. evalexpr's `=` operator rejects a
13/// function-call LHS with a confusing "Expected String, got Float" error,
14/// which cascades across the rest of the equation. The two-arg call
15/// shape is well-formed; `gmegabuf_set` / `megabuf_set` are registered
16/// as buffer-backed functions in `math_functions.rs`.
17///
18/// Walker-based, replacing an earlier regex-only implementation. Three gains
19/// over the regex:
20///
21/// 1. **Nested parens in `<idx>`.** The old regex's `[^()=]+?` class
22///    rejected any `(` / `)` inside the index expression, so
23///    `gmegabuf((bd_src*bd_oct+oct)*2) = bd_qual` silently fell through
24///    and tripped evalexpr's "Expected String, got Float" error.
25/// 2. **Compound assignments.** `gmegabuf(idx) += val` / `*=` / `-=` /
26///    `/=` are common in the corpus and now expand to
27///    `gmegabuf_set(idx, gmegabuf(idx) <op> val)`.
28/// 3. **Allows whitespace between `name` and `(`.** Corpus presets
29///    write `gmegabuf (n+1)` with a space.
30pub(super) fn rewrite_gmegabuf_writes(s: &str) -> String {
31    let bytes = s.as_bytes();
32    let mut out = String::with_capacity(bytes.len() + 16);
33    let mut i = 0usize;
34    while i < bytes.len() {
35        // Match `gmegabuf` or `megabuf` identifier, preceded by a non-ident
36        // byte (or start-of-string).
37        if let Some((name, name_end)) = match_megabuf_name(bytes, i)
38            && (i == 0 || !is_ident_byte(bytes[i - 1]))
39        {
40            let mut j = name_end;
41            while j < bytes.len() && matches!(bytes[j], b' ' | b'\t') {
42                j += 1;
43            }
44            if j < bytes.len()
45                && bytes[j] == b'('
46                && let Some(close) = match_close_paren(bytes, j)
47            {
48                let idx = s[j + 1..close].trim();
49                let mut k = close + 1;
50                while k < bytes.len() && matches!(bytes[k], b' ' | b'\t') {
51                    k += 1;
52                }
53                // Detect `=` (single, not `==`) or compound `+= -= *= /=`.
54                if let Some((op, after_eq)) = detect_assignment_op(bytes, k) {
55                    let val_end = find_assignment_value_end(bytes, after_eq);
56                    let value = s[after_eq..val_end].trim();
57                    if let Some(op_char) = op {
58                        // Compound: `name(idx) <op>= val` →
59                        // `name_set(idx, name(idx) <op> val)`.
60                        out.push_str(&format!(
61                            "{name}_set({idx}, {name}({idx}) {} {value})",
62                            op_char as char
63                        ));
64                    } else {
65                        out.push_str(&format!("{name}_set({idx}, {value})"));
66                    }
67                    i = val_end;
68                    continue;
69                }
70            }
71        }
72        out.push(bytes[i] as char);
73        i += 1;
74    }
75    out
76}
77
78/// Match `gmegabuf` or `megabuf` starting at byte offset `pos`. Returns
79/// the matched name slice and the byte just past it, or `None` if no
80/// match.
81fn match_megabuf_name(bytes: &[u8], pos: usize) -> Option<(&'static str, usize)> {
82    // Guard against ident-continuers so we don't match `gmegabuf_set`.
83    if pos + 8 <= bytes.len()
84        && &bytes[pos..pos + 8] == b"gmegabuf"
85        && (pos + 8 == bytes.len() || !is_ident_byte(bytes[pos + 8]))
86    {
87        return Some(("gmegabuf", pos + 8));
88    }
89    if pos + 7 <= bytes.len()
90        && &bytes[pos..pos + 7] == b"megabuf"
91        && (pos + 7 == bytes.len() || !is_ident_byte(bytes[pos + 7]))
92    {
93        return Some(("megabuf", pos + 7));
94    }
95    None
96}
97
98/// Detect an assignment operator starting at byte offset `pos` in
99/// `bytes`. Returns `Some((Some(op_char), after_eq))` for compound
100/// assignments (`+=`, `-=`, `*=`, `/=`), `Some((None, after_eq))` for
101/// plain `=`, or `None` if no recognised assignment op (or `==` /
102/// other comparison) starts here.
103fn detect_assignment_op(bytes: &[u8], pos: usize) -> Option<(Option<u8>, usize)> {
104    let first = bytes.get(pos).copied()?;
105    let second = bytes.get(pos + 1).copied();
106    match first {
107        b'=' if second != Some(b'=') => Some((None, pos + 1)),
108        b'+' | b'-' | b'*' | b'/' if second == Some(b'=') => Some((Some(first), pos + 2)),
109        _ => None,
110    }
111}
112
113/// Find the byte offset where an assignment's RHS value ends. Walks
114/// forward through `bytes` starting at `offset`, tracking paren balance.
115/// Stops at the first `;` or `,` at depth 0, an unmatched `)` (the
116/// assignment lives inside an enclosing call), or end-of-buffer.
117fn find_assignment_value_end(bytes: &[u8], offset: usize) -> usize {
118    let mut depth = 0i32;
119    let mut i = offset;
120    while i < bytes.len() {
121        match bytes[i] {
122            b'(' => depth += 1,
123            b')' => {
124                if depth == 0 {
125                    return i;
126                }
127                depth -= 1;
128            }
129            b';' | b',' if depth == 0 => return i,
130            _ => {}
131        }
132        i += 1;
133    }
134    bytes.len()
135}