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