Skip to content

Normalize ordinary string literals at the frontend - #8603

Open
cknitt wants to merge 8 commits into
rescript-lang:masterfrom
cknitt:codex/fix-equivalent-string-patterns
Open

Normalize ordinary string literals at the frontend#8603
cknitt wants to merge 8 commits into
rescript-lang:masterfrom
cknitt:codex/fix-equivalent-string-patterns

Conversation

@cknitt

@cknitt cknitt commented Aug 31, 2026

Copy link
Copy Markdown
Member

First part of the String Theory series, which simplifies and normalizes string-literal handling across the compiler.

Summary

String literals currently retain their encoded source spelling after parsing. Consequently, equivalent values such as "a" and "\x61" can be treated differently by pattern matching, constant folding, and other compiler consumers.

This PR normalizes ordinary string literals to their decoded semantic value at the frontend boundary. As a result:

  • equivalent string patterns are recognized as redundant without repeatedly decoding them during comparison;
  • string switches with constant scrutinees fold using runtime values;
  • guarded equivalent patterns preserve source-order behavior;
  • ordinary expressions and patterns share one string representation;
  • the internal DStarJ representation is no longer needed;
  • valid UTF-16 surrogate-pair escapes are supported, while lone and malformed surrogates remain errors.

Tagged template literals are now rejected in patterns because a pattern cannot invoke its tag, and treating the raw payload as a string could produce incorrect matches.

The ast0 bridge converts ordinary literals to the normalized representation while preserving source spelling for templates and the compiler-reserved %raw, %%raw, %ffi, and %re extensions. This keeps external PPX round trips compatible without changing escape-sensitive JavaScript payloads.

GenType emission has also been updated to escape semantic @as values correctly.

This implements the normalization boundary proposed in #8602. Later parts of the series can build on it to introduce more explicit representations for templates and other non-semantic source payloads.

Tests

  • Added decoder coverage for supported escapes, line continuations, invalid inputs, and surrogate pairs.
  • Added ast0 conversion and round-trip coverage for ordinary strings, templates, and raw extensions.
  • Added warning and error fixtures for equivalent patterns and tagged-template patterns.
  • Added end-to-end coverage for matching, constant folding, surrogate pairs, external PPX conversion, and escape-sensitive raw JavaScript.
  • Verified with:
    • opam exec -- make test
    • opam exec -- make test-syntax
    • opam exec -- make test-gentype
    • opam exec -- make test-analysis

cknitt added a commit to cknitt/rescript that referenced this pull request Aug 31, 2026
Signed-off-by: Christoph Knittel <ck@cca.io>
@codecov

codecov Bot commented Aug 31, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 85.18519% with 44 lines in your changes missing coverage. Please review.
✅ Project coverage is 76.53%. Comparing base (59372ec) to head (d712635).

Files with missing lines Patch % Lines
compiler/syntax/src/res_scanner.ml 43.75% 9 Missing ⚠️
compiler/ml/ast_mapper_from0.ml 75.00% 8 Missing ⚠️
compiler/gentype/import_path.ml 57.14% 6 Missing ⚠️
compiler/ml/string_literal.ml 91.54% 6 Missing ⚠️
tests/ounit_tests/ounit_ast_mapper0_tests.ml 90.90% 5 Missing ⚠️
tests/ounit_tests/ounit_string_literal_tests.ml 91.93% 5 Missing ⚠️
compiler/gentype/translate_type_declarations.ml 60.00% 2 Missing ⚠️
compiler/syntax/src/res_printer.ml 50.00% 2 Missing ⚠️
compiler/syntax/src/res_outcome_printer.ml 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #8603      +/-   ##
==========================================
+ Coverage   76.44%   76.53%   +0.08%     
==========================================
  Files         478      481       +3     
  Lines       63163    63438     +275     
==========================================
+ Hits        48286    48553     +267     
- Misses      14877    14885       +8     
Files with missing lines Coverage Δ
compiler/core/j.ml 100.00% <ø> (ø)
compiler/core/js_dump.ml 88.08% <ø> (-0.02%) ⬇️
compiler/core/js_exp_make.ml 64.20% <100.00%> (-0.28%) ⬇️
compiler/frontend/ast_utf8_string_interp.ml 69.64% <100.00%> (+2.05%) ⬆️
compiler/gentype/emit_text.ml 95.00% <100.00%> (+9.28%) ⬆️
compiler/gentype/translate_core_type.ml 76.92% <100.00%> (+1.92%) ⬆️
compiler/ml/ast_mapper_to0.ml 62.93% <100.00%> (+0.55%) ⬆️
compiler/ml/external_arg_spec.ml 100.00% <ø> (ø)
tests/ounit_tests/ounit_gentype_tests.ml 100.00% <100.00%> (ø)
tests/ounit_tests/ounit_lambda_constant_tests.ml 100.00% <ø> (ø)
... and 10 more

... and 8 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@pkg-pr-new

pkg-pr-new Bot commented Aug 31, 2026

Copy link
Copy Markdown

Open in StackBlitz

rescript

npm i https://pkg.pr.new/rescript-lang/rescript@8603

@rescript/belt

npm i https://pkg.pr.new/rescript-lang/rescript/@rescript/belt@8603

@rescript/darwin-arm64

npm i https://pkg.pr.new/rescript-lang/rescript/@rescript/darwin-arm64@8603

@rescript/darwin-x64

npm i https://pkg.pr.new/rescript-lang/rescript/@rescript/darwin-x64@8603

@rescript/linux-arm64

npm i https://pkg.pr.new/rescript-lang/rescript/@rescript/linux-arm64@8603

@rescript/linux-x64

npm i https://pkg.pr.new/rescript-lang/rescript/@rescript/linux-x64@8603

@rescript/runtime

npm i https://pkg.pr.new/rescript-lang/rescript/@rescript/runtime@8603

@rescript/win32-x64

npm i https://pkg.pr.new/rescript-lang/rescript/@rescript/win32-x64@8603

commit: d712635

@cknitt
cknitt marked this pull request as ready for review August 31, 2026 05:27
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Aug 31, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-08-31T15:23:26.092253Z c2aad32 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 22bf547224

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread compiler/ml/string_literal.ml
@cknitt

cknitt commented Aug 31, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ede3c1ede3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread compiler/ml/string_literal.ml Outdated
@cknitt

cknitt commented Aug 31, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: de20e15933

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread compiler/ml/string_literal.ml Outdated
@cknitt

cknitt commented Aug 31, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9a7d63d24a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@cristianoc cristianoc left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice fix for the core issue — I confirmed the headline behavior: | "a" => 1 | "\x61" => 2 now returns 1 for "a", where master returned 2. The decode_js_escapes decoder also looks solid; I went through \x, \u, \u{...}, surrogate pairs, line continuations and the overflow guard in the braced form and did not find an arithmetic problem.

I built the branch and checked the findings below against a PR-built bsc with a master-built one as control. Two of them I think are blocking.

Blocking

1. Unrecognized -> raise_errorf breaks bsc -reprint-source on any file containing a char pattern

transform_pat's new catch-all rejects every delimiter that isn't "js", but the printer path uses a delimiter of its own. In printer mode (p.mode <> ParseForTypeChecker), res_core.ml:1037 encodes char literals as:

Pconst_string (original, Some "INTERNAL_RES_CHAR_CONTENTS")

and bs_builtin_ppx.ml:97 forwards every Ppat_constant (Pconst_string (s, Some delim)) to transform_pat unconditionally. Since Delim.parse_unprocessed returns Unrecognized for anything but "js", a plain char pattern now hard-errors:

switch x {
| 'a' => 1
| _ => 2
}
master: bsc -reprint-source ch.res  ->  prints the source
PR:     bsc -reprint-source ch.res  ->  "Tagged template literals are not supported in patterns" at 'a'

The guard needs to reject actual tag delimiters specifically rather than everything that isn't "js".

2. The fix is incomplete — constant-scrutinee string switches still fold by encoded form

const_compare is only one of the places that compares string constants. Js_stmt_make.string_switch (js_stmt_make.ml:150) selects a clause with a raw s = txt comparison, and Lam.stringswitch (lam.ml:326) uses Ext_list.assoc_by_string; neither decodes escapes. On this branch:

let h = () =>
  switch "a" {
  | "\x61" => 1
  | "b" => 2
  | "c" => 3
  | _ => 4
  }

compiles to function h() { return 4; }. It should be 1, since "a" === "\x61" in JS. That is the same bug class the PR is fixing, so it would be good to either cover it here or note it explicitly as out of scope.

Performance

3. ~8x compile-time regression on large string matches

decode_js_escapes allocates a Buffer and rebuilds both operands on every comparison, and const_compare is called O(n^2) times by parmatch's redundancy analysis on top of the O(n log n) from sort_lambda_list. On a generated 2000-case string switch containing no escapes at all:

master: 0.11s
PR:     0.93s
control, 2000-case int switch: master 0.15s / PR 0.11s

A fast path in runtime_value should recover this — return s unchanged when not (String.contains s '\\'), before allocating anything.

Smaller points

  1. Tagged template patterns become a hard error without a breaking-change note. switch x { | json`abc` => 1 | _ => 2 } compiles on master (to x === "abc") and is a compile error here. If that is intended, it wants a #### :boom: Breaking Change CHANGELOG entry and a tests/build_tests/super_errors/ fixture — right now the new error is only exercised by an OUnit test calling transform_pat directly. Both new raise_errorf sites also need rows in tests/ERROR_VARIANTS.md.

  2. Surrogate pairs are a new feature, not just a pattern fix. The res_scanner.ml change applies to all string literals, so let x = "\uD83D\uDE00" compiles now and was a scanner error before. Worth its own CHANGELOG line and a test outside pattern position.

  3. Duplicate diagnostics for one malformed surrogate escape. In the new \u branch, scan_digits has already reported "unknown escape sequence" and returned -1 before the caller tests low < 0xDC00 and reports "escape sequence is invalid unicode code point" on top. "\uD83D\uD83D\uDE00" yields two errors for one bad escape; gating the second report on low >= 0 would fix it.

  4. runtime_value silently aliases undecodable literals to their raw text. Returning s on None means an undecodable literal can compare equal to a different literal whose decoded value happens to match that raw text. This is unreachable today only because transform_pat rejects undecodable *j patterns — an implicit cross-module invariant. Worth documenting in the .mli, or having compare distinguish decodable from undecodable operands.

  5. Minor: in transform_pat the decode result is discarded (Some _ -> ()) and then recomputed by runtime_value during comparison. And in test_string_switch.mjs the collapsed case emits value === "\x61" rather than value === "a" — runtime-identical, but the emitted spelling comes from a pattern that warning 11 reports as unused, so the output depends on dead code.

@cristianoc

Copy link
Copy Markdown
Collaborator

There's ongoing cleanup #8604 that also touches const strings. Does not seem to conflict right now, but just wanted to mention it. I have put it up for review so it will get out of the way in any case.

@cknitt

cknitt commented Aug 31, 2026

Copy link
Copy Markdown
Member Author

Thanks!
A good part of the reported issues is already solved in later parts of the series that have no PR yet.

Will pull the relevant parts into this PR to avoid churn.

@cristianoc

Copy link
Copy Markdown
Collaborator

One suggestion below that seems sensible at face value. Haven't verified it deeply.


The trouble is that Pconst_string (s, delim) conflates two different things under one field. s is the source spelling — escape text, still undecoded — and delim is an unparsed marker meaning "this hasn't been resolved yet." Every downstream consumer that cares about the string's value therefore has to re-derive it. 8603 teaches exactly one consumer (parmatch.const_compare) to do that, which is why the fix is simultaneously incomplete, quadratic, and dependent on an unenforceable cross-module invariant. Those three findings aren't independent defects; they're the same defect observed from three angles.

The right time to resolve is the moment the literal stops being syntax and becomes a value — the frontend boundary, in Ast_utf8_string.transform and its transform_exp/transform_pat callers. That's where the delimiter is consumed, and, importantly, that code already walks the string character by character to validate escapes and copy them into a buffer. Producing the decoded value there is free: the traversal is happening regardless. It's also the only place that still has a source location, so a bad escape becomes a proper error rather than a None that has to be defended against three modules later.

cknitt added a commit to cknitt/rescript that referenced this pull request Aug 31, 2026
Signed-off-by: Christoph Knittel <ck@cca.io>
@cknitt
cknitt force-pushed the codex/fix-equivalent-string-patterns branch from 9a7d63d to ad78db8 Compare August 31, 2026 13:13
@cknitt

cknitt commented Aug 31, 2026

Copy link
Copy Markdown
Member Author

Addressed in ad78db8 by pulling the ordinary-string normalization forward into this PR.

The main change is that ordinary string literals are decoded once at the frontend boundary and stored as semantic strings from then on. This addresses the incomplete-fix, performance, fallback, duplicate-decoding, and dead-spelling points together:

  • Character patterns using INTERNAL_RES_CHAR_CONTENTS remain untouched; the exact -reprint-source reproducer now succeeds and has regression coverage.
  • Constant-scrutinee switches now fold using semantic values; the reported example is covered end to end and returns 1.
  • Pattern comparison is plain String.compare again. There is no decoder work in the O(n²) comparison path.
  • Tagged template patterns are rejected with a breaking-change changelog entry and a super-errors fixture.
  • Surrogate-pair support has its own feature changelog entry and ordinary-expression coverage.
  • Malformed low-surrogate scans gate the follow-up diagnostic on low >= 0; the scanner snapshot verifies a single diagnostic.
  • runtime_value and its undecodable fallback are gone.
  • The decoded result is retained rather than discarded, and generated switch spelling now follows the live semantic value.
  • ast0 conversion decodes ordinary legacy js/*j strings while preserving template source through the template attribute, with round-trip tests.

I also added warning 11 to tests/ERROR_VARIANTS.md. The two Location.raise_errorf diagnostics are covered by fixtures/tests, but are not rows in that catalog because it catalogs named compiler error and warning variants rather than ad-hoc Location errors.

Local verification: make test, make test-syntax, and make test-analysis all pass with the OCaml 5.5.0 switch.

@cknitt

cknitt commented Aug 31, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ad78db88ca

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread compiler/ml/ast_mapper_from0.ml
@cknitt

cknitt commented Aug 31, 2026

Copy link
Copy Markdown
Member Author

Follow-up: the first CI run exposed one more downstream encoded-string assumption in GenType. Semantic @as variant labels were emitted without TypeScript escaping, while record-key snapshots still reflected the old encoded representation. Fixed in bb12ab8; make test-gentype now passes locally, and the complete downstream branch stack has been rebased again.

@cknitt

cknitt commented Aug 31, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1269f0f899

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread compiler/gentype/translate_type_declarations.ml Outdated
Comment thread compiler/frontend/ast_utf8_string_interp.ml
@cknitt cknitt changed the title Fix pattern matching for equivalent string literals Normalize ordinary string literals at the frontend Aug 31, 2026
cknitt added 2 commits August 31, 2026 16:40
Signed-off-by: Christoph Knittel <ck@cca.io>
Signed-off-by: Christoph Knittel <ck@cca.io>
cknitt added 3 commits August 31, 2026 16:40
Signed-off-by: Christoph Knittel <ck@cca.io>
Signed-off-by: Christoph Knittel <ck@cca.io>
Signed-off-by: Christoph Knittel <ck@cca.io>
@cknitt
cknitt force-pushed the codex/fix-equivalent-string-patterns branch from 1269f0f to c2aad32 Compare August 31, 2026 15:10
@cknitt

cknitt commented Aug 31, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c2aad326c8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread compiler/gentype/translate_type_declarations.ml Outdated
cknitt added 3 commits August 31, 2026 17:49
Signed-off-by: Christoph Knittel <ck@cca.io>
Signed-off-by: Christoph Knittel <ck@cca.io>
Signed-off-by: Christoph Knittel <ck@cca.io>
@cknitt
cknitt force-pushed the codex/fix-equivalent-string-patterns branch from c2aad32 to d712635 Compare August 31, 2026 15:51
@cknitt

cknitt commented Aug 31, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants