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}