From 825a83e0098bda91052de392c0b2f605f57d5650 Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Sun, 7 Jun 2026 10:53:50 +0200 Subject: [PATCH] fix(beam): make Emit $N substitution a single left-to-right pass 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) --- src/Fable.Transforms/Beam/ErlangPrinter.fs | 35 ++++++++++++----- tests/Beam/InteropTests.fs | 45 ++++++++++++++++++++++ 2 files changed, 71 insertions(+), 9 deletions(-) diff --git a/src/Fable.Transforms/Beam/ErlangPrinter.fs b/src/Fable.Transforms/Beam/ErlangPrinter.fs index 8e034b88a2..fc2192ddce 100644 --- a/src/Fable.Transforms/Beam/ErlangPrinter.fs +++ b/src/Fable.Transforms/Beam/ErlangPrinter.fs @@ -466,15 +466,32 @@ module Output = sb.Append("end") |> ignore | Emit(template, args) -> - // Substitute $0, $1, etc. with printed argument expressions - let mutable result = template - - args - |> List.iteri (fun i arg -> - let argSb = System.Text.StringBuilder() - printExpr argSb indent arg - result <- result.Replace($"$%d{i}", argSb.ToString()) - ) + // Substitute $0, $1, etc. with printed argument expressions. + // Done in a single left-to-right pass so that (a) `$1` does not + // corrupt `$10` (the full integer index is parsed) and (b) a `$N` + // sequence appearing inside an already-substituted argument (e.g. a + // string literal containing "$1") is not re-substituted. + let printedArgs = + args + |> List.map (fun arg -> + let argSb = System.Text.StringBuilder() + printExpr argSb indent arg + argSb.ToString() + ) + |> List.toArray + + 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 + // Out-of-range placeholder: leave the literal text untouched. + | None -> m.Value + ) // Erlang has flat variable scoping — variables bound inside case/begin // blocks leak into the surrounding function clause. When the same Emit diff --git a/tests/Beam/InteropTests.fs b/tests/Beam/InteropTests.fs index eb670c1f36..28e7813a17 100644 --- a/tests/Beam/InteropTests.fs +++ b/tests/Beam/InteropTests.fs @@ -20,6 +20,30 @@ let listLength (xs: 'a list) : int = nativeOnly [>")>] let concatBinaries (a: string) (b: string) : string = nativeOnly +// Substitution must be a single left-to-right pass: +// a `$N` sequence appearing inside an already-substituted argument +// (here the string literal "$1") must NOT be re-substituted. +[] +let emitPair (a: string) (b: int) : string * int = nativeOnly + +// `$1` substitution must not corrupt the `$10` placeholder (full integer +// index is parsed, not a naive textual replace). +[] +let emitIndex10 + (a0: int) + (a1: int) + (a2: int) + (a3: int) + (a4: int) + (a5: int) + (a6: int) + (a7: int) + (a8: int) + (a9: int) + (a10: int) + : int list = + nativeOnly + // ============================================================ // Import tests // ============================================================ @@ -151,6 +175,27 @@ let ``test Emit can use binary syntax`` () = () #endif +[] +let ``test Emit does not re-substitute $N inside argument text`` () = +#if FABLE_COMPILER + // The string argument "$1" must survive verbatim — the substituter must + // not replace the "$1" that appears inside the already-printed argument. + let a, b = emitPair "$1" 7 + equal "$1" a + equal 7 b +#else + () +#endif + +[] +let ``test Emit $1 does not corrupt $10 placeholder`` () = +#if FABLE_COMPILER + let result = emitIndex10 0 99 2 3 4 5 6 7 8 9 1010 + equal [ 0; 99; 1010 ] result +#else + () +#endif + [] let ``test emitErlExpr works`` () = #if FABLE_COMPILER