You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(codegen-js): emit \uXXXX/\u{X} for non-ASCII string literals (closes#460) (#463)
## Summary
Closes#460 — non-ASCII string literals in AffineScript source no longer
break strict-mode ESM in the Deno/Node JS backends.
## Root cause
OCaml's \`String.escaped\` emits non-ASCII bytes as \`\\NNN\`
**decimal** sequences. JavaScript parses \`\\NNN\` as **octal** escapes
which strict-mode ESM rejects:
\`\`\`
SyntaxError: Octal escape sequences are not allowed in strict mode.
\`\`\`
(And even outside strict mode the bytes would decode to the wrong
characters — \`\\226\` octal = 0x96, not the 0xE2 lead-byte of ❌.)
## Fix
New helper \`Js_codegen.js_string_lit\` walks the UTF-8 byte sequence,
decodes code points, and emits:
| Character class | Output |
|---|---|
| Printable ASCII (0x20-0x7E except \`\\\` \`\"\`) | as-is |
| \`\\\` \`\"\` \`\n\` \`\r\` \`\t\` | conventional escape |
| Other ASCII (control bytes) | \`\\xHH\` |
| Non-ASCII BMP (U+0080..U+FFFF) | \`\\uXXXX\` |
| Non-BMP (U+10000+) | \`\\u{XXXXX}\` |
Wired into both \`js_codegen.ml\` (Node target) and \`codegen_deno.ml\`
(Deno-ESM target) at the \`LitString\`/\`LitChar\` emit sites.
## Test plan
New \`tests/codegen-deno/non_ascii.affine\` fixture + harness:
\`\`\`affine
pub fn emoji_cross() -> String { return \"❌\"; } // BMP U+274C
pub fn non_bmp_sob() -> String { return \"😭\"; } // non-BMP U+1F62D
pub fn cjk_hello() -> String { return \"你好\"; }
pub fn latin_accent() -> String { return \"café résumé\"; }
pub fn mixed() -> String { return \"[OK] café 你好 ❌\"; }
pub fn ascii_only() -> String { return \"plain ASCII\"; }
pub fn quotes_and_backslash() -> String { return \"\\\"escaped\\\" and
\\\\back\"; }
\`\`\`
The \`import\` itself is the strictest test: if the emitted \`.deno.js\`
contains octal escapes, the module fails to parse and the harness import
throws SyntaxError before any assertion runs.
- [x] Local \`./tools/run_codegen_deno_tests.sh\`: **13/13** harnesses
green (including the new fixture)
- [x] Local \`dune test\`: **352/352** unit tests green
- [x] Compiler output spot-check: \`emoji_cross\` emits \`return
\"\\u274C\";\`, \`non_bmp_sob\` emits \`return \"\\u{1F62D}\";\`, ASCII
passes through unchanged
- [x] Manual: emitted \`.deno.js\` parses + runs under Node 20 ESM
(which uses strict mode by default)
## Out of scope
- \`rescript_codegen.ml\` also uses \`String.escaped\` but emits
ReScript source (which the rescript compiler then transforms to JS).
Whether ReScript inherits the same bug is a separate question; not
addressed here.
- Other non-JS codegens (lua, c, rust, julia, gleam, nickel, why3) keep
\`String.escaped\` — they target languages with their own escape
conventions.
## Refs
- Closes#460 (the gap)
- Refs hyperpolymath/standards#284 (the seam-analyst PR that surfaced
this — worked around with ASCII \`[FAIL]\`/\`[OK]\` sentinels)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
0 commit comments