Skip to main content

incurs_codemode/
normalize.rs

1/// Normalizes model-generated JavaScript into an async zero-argument function.
2pub fn normalize_code(code: &str) -> String {
3    let source = strip_fence(code.trim()).trim();
4    if source.is_empty() {
5        return "async () => {}".to_string();
6    }
7    if is_arrow(source) {
8        return source.to_string();
9    }
10    if let Some(inner) = source.strip_prefix("export default ") {
11        return normalize_code(inner.trim_end_matches(';'));
12    }
13    if let Some(name) = single_function_name(source) {
14        return format!("async () => {{\n{source}\nreturn {name}();\n}}");
15    }
16    if let Some((before, expression)) = split_last_expression(source) {
17        return format!("async () => {{\n{before}return ({expression})\n}}");
18    }
19    format!("async () => {{\n{source}\n}}")
20}
21
22fn strip_fence(source: &str) -> &str {
23    let Some(after) = source.strip_prefix("```") else {
24        return source;
25    };
26    let Some(newline) = after.find('\n') else {
27        return source;
28    };
29    let body = &after[newline + 1..];
30    body.strip_suffix("```")
31        .map(str::trim_end)
32        .unwrap_or(source)
33}
34
35fn is_arrow(source: &str) -> bool {
36    let Some(arrow) = top_level_arrow(source) else {
37        return false;
38    };
39    let prefix = source[..arrow].trim();
40    prefix.ends_with(')') || prefix.starts_with("async ") || is_identifier(prefix)
41}
42
43fn top_level_arrow(source: &str) -> Option<usize> {
44    let mut depth = 0;
45    let mut quote = None;
46    let mut escaped = false;
47    let mut chars = source.char_indices().peekable();
48    while let Some((index, ch)) = chars.next() {
49        if let Some(active) = quote {
50            if escaped {
51                escaped = false;
52            } else if ch == '\\' {
53                escaped = true;
54            } else if ch == active {
55                quote = None;
56            }
57            continue;
58        }
59        match ch {
60            '\'' | '"' | '`' => quote = Some(ch),
61            '(' | '[' | '{' => depth += 1,
62            ')' | ']' | '}' => depth -= 1,
63            '=' if depth == 0 && chars.peek().is_some_and(|(_, next)| *next == '>') => {
64                return Some(index);
65            }
66            _ => {}
67        }
68    }
69    None
70}
71
72fn single_function_name(source: &str) -> Option<&str> {
73    let rest = source
74        .strip_prefix("async function ")
75        .or_else(|| source.strip_prefix("function "))?;
76    let end = rest.find('(')?;
77    let name = rest[..end].trim();
78    if is_identifier(name) && balanced(source) {
79        Some(name)
80    } else {
81        None
82    }
83}
84
85fn split_last_expression(source: &str) -> Option<(&str, &str)> {
86    if !balanced(source) || source.ends_with('}') {
87        return None;
88    }
89    let mut depth = 0;
90    let mut quote = None;
91    let mut escaped = false;
92    let mut split = None;
93    for (index, ch) in source.char_indices() {
94        if let Some(active) = quote {
95            if escaped {
96                escaped = false;
97            } else if ch == '\\' {
98                escaped = true;
99            } else if ch == active {
100                quote = None;
101            }
102            continue;
103        }
104        match ch {
105            '\'' | '"' | '`' => quote = Some(ch),
106            '(' | '[' | '{' => depth += 1,
107            ')' | ']' | '}' => depth -= 1,
108            ';' if depth == 0 => split = Some(index + 1),
109            _ => {}
110        }
111    }
112    let mut index = split.unwrap_or(0);
113    while source[index..]
114        .chars()
115        .next()
116        .is_some_and(char::is_whitespace)
117    {
118        index += source[index..].chars().next().unwrap().len_utf8();
119    }
120    let expression = source[index..].trim().trim_end_matches(';').trim();
121    if expression.is_empty() || starts_statement(expression) {
122        None
123    } else {
124        Some((&source[..index], expression))
125    }
126}
127
128fn starts_statement(source: &str) -> bool {
129    [
130        "const ",
131        "let ",
132        "var ",
133        "return ",
134        "throw ",
135        "if ",
136        "for ",
137        "while ",
138        "class ",
139        "function ",
140        "import ",
141        "export ",
142        "try ",
143        "switch ",
144    ]
145    .iter()
146    .any(|prefix| source.starts_with(prefix))
147}
148
149fn balanced(source: &str) -> bool {
150    let mut stack = Vec::new();
151    let mut quote = None;
152    let mut escaped = false;
153    for ch in source.chars() {
154        if let Some(active) = quote {
155            if escaped {
156                escaped = false;
157            } else if ch == '\\' {
158                escaped = true;
159            } else if ch == active {
160                quote = None;
161            }
162            continue;
163        }
164        match ch {
165            '\'' | '"' | '`' => quote = Some(ch),
166            '(' | '[' | '{' => stack.push(ch),
167            ')' if stack.pop() != Some('(') => return false,
168            ']' if stack.pop() != Some('[') => return false,
169            '}' if stack.pop() != Some('{') => return false,
170            _ => {}
171        }
172    }
173    stack.is_empty() && quote.is_none()
174}
175
176fn is_identifier(value: &str) -> bool {
177    let mut chars = value.chars();
178    chars
179        .next()
180        .is_some_and(|ch| ch == '_' || ch == '$' || ch.is_ascii_alphabetic())
181        && chars.all(|ch| ch == '_' || ch == '$' || ch.is_ascii_alphanumeric())
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187
188    #[test]
189    fn normalizes_common_model_outputs() {
190        assert_eq!(normalize_code(""), "async () => {}");
191        assert_eq!(normalize_code("async () => 1"), "async () => 1");
192        assert_eq!(
193            normalize_code("const x = 1;\nx + 2"),
194            "async () => {\nconst x = 1;\nreturn (x + 2)\n}"
195        );
196        assert_eq!(
197            normalize_code("```js\nstate.read({ id: 1 })\n```"),
198            "async () => {\nreturn (state.read({ id: 1 }))\n}"
199        );
200        assert_eq!(
201            normalize_code(
202                "const token = await codemode.step(\"token\", async () => \"stable\");\nreturn token;"
203            ),
204            "async () => {\nconst token = await codemode.step(\"token\", async () => \"stable\");\nreturn token;\n}"
205        );
206    }
207}