Skip to content

fix(beam): make Emit $N substitution a single left-to-right pass#4631

Merged
dbrattli merged 1 commit into
mainfrom
fix/beam-emit-substitution
Jun 7, 2026
Merged

fix(beam): make Emit $N substitution a single left-to-right pass#4631
dbrattli merged 1 commit into
mainfrom
fix/beam-emit-substitution

Conversation

@dbrattli
Copy link
Copy Markdown
Collaborator

@dbrattli dbrattli commented Jun 7, 2026

Problem

The Erlang/BEAM printer substituted [<Emit>] placeholders with naive sequential String.Replace($"$%d{i}", ...). Iterating one placeholder at a time over the mutated string has two correctness bugs:

  1. $1 corrupts $10 (textual prefix match). Replacing $1 also matches the $1 prefix of $10:
    [<Emit("[$0, $1, $10]")>]
    let f a0 a1 ... a10 = nativeOnly
    f 0 99 ... 1010   // produced [0,99,990] instead of [0,99,1010]
  2. A $N sequence inside an already-substituted argument gets re-substituted:
    [<Emit("{$0, $1}")>]
    let pair (a: string) (b: int) = nativeOnly
    pair "$1" 7       // produced {<<"7">>, 7} instead of {<<"$1">>, 7}

Fix

Replace the loop with a single Regex.Replace over \$\d+ — the same approach the JS printer (BabelPrinter.fs) already uses:

let result =
    System.Text.RegularExpressions.Regex.Replace(
        template,
        @"\$\d+",
        fun m ->
            let idx = int (m.Value.Substring(1))
            match Array.tryItem idx printedArgs with
            | Some printed -> printed
            | None -> m.Value   // out-of-range placeholder: leave literal text untouched
    )

This fixes both bugs by construction: \d+ matches the full integer index (so $10 isn't clipped by $1), and Regex.Replace evaluates matches against the original template only, so a $N appearing inside a substituted argument is never re-scanned.

Scope is limited to the Erlang printer (ErlangPrinter.fs); no other backend is touched.

Tests

Added two regression tests in tests/Beam/InteropTests.fs covering both bugs. Full BEAM suite passes (2444 passed, 0 failed).

🤖 Generated with Claude Code

The Erlang printer substituted Emit placeholders with naive sequential
String.Replace($"$%d{i}", ...), which had two correctness bugs:

- `$1` corrupted `$10` (textual prefix match): `[$0,$1,$10]` produced
  `[0,99,990]` instead of `[0,99,1010]`.
- A `$N` sequence inside an already-substituted argument was re-substituted:
  `pair "$1" 7` produced `{<<"7">>, 7}` instead of `{<<"$1">>, 7}`.

Replace it with a single left-to-right scan that parses the full integer
index and never re-scans substituted text, matching the JS printer's
single-pass behaviour. Beam-only, printer-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@dbrattli dbrattli force-pushed the fix/beam-emit-substitution branch from 86a2da1 to 825a83e Compare June 7, 2026 08:57
@dbrattli dbrattli merged commit 48af8db into main Jun 7, 2026
31 checks passed
@dbrattli dbrattli deleted the fix/beam-emit-substitution branch June 7, 2026 09:11
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.

1 participant