diff --git a/packages/compiler/src/frontend/lowering/containers/replacer-params.ts b/packages/compiler/src/frontend/lowering/containers/replacer-params.ts index e84ca1461..3a3eff1cf 100644 --- a/packages/compiler/src/frontend/lowering/containers/replacer-params.ts +++ b/packages/compiler/src/frontend/lowering/containers/replacer-params.ts @@ -115,7 +115,10 @@ const STRING_MEMBERS: ReadonlySet = new Set([ /** Whether every read of `symbol` in `body` is one that means the same * for an `any` value and for a `kind` value, and lowers for both: a - * template span, a Number/String/parseInt/parseFloat argument, a listed + * template span, the argument of Number or String, the text argument of + * parseInt or parseFloat for a string and the radix of parseInt for a + * number (each a parameter of that type, which an `any` value converts + * to), a listed * string member, `+`, strict equality, arithmetic and comparisons of * numbers, a returned value, and a string key of an element access on a * string-indexed record or an `any` value. Anything else keeps the @@ -144,13 +147,20 @@ function readsFit( return shape?.indexValue !== undefined && !shape.tuple; } if (ts.isArrowFunction(parent)) return parent.body === node; - if (ts.isCallExpression(parent)) - return ( - parent.expression !== node && - ["Number", "String", "parseInt", "parseFloat"].some((global) => - lowerer.isStdlibGlobal(parent.expression, global), - ) - ); + if (ts.isCallExpression(parent)) { + if (parent.expression === node) return false; + const position = parent.arguments.nodes.indexOf(node as ts.Expression); + if ( + lowerer.isStdlibGlobal(parent.expression, "Number") || + lowerer.isStdlibGlobal(parent.expression, "String") + ) + return position === 0; + if (lowerer.isStdlibGlobal(parent.expression, "parseFloat")) + return position === 0 && kind === "string"; + if (lowerer.isStdlibGlobal(parent.expression, "parseInt")) + return position === 0 ? kind === "string" : position === 1 && kind === "f64"; + return false; + } if (ts.isPropertyAccessExpression(parent)) { const member = parent.name.text; if (kind !== "string" || parent.expression !== node || !STRING_MEMBERS.has(member)) diff --git a/packages/compiler/test/replacer-params.test.ts b/packages/compiler/test/replacer-params.test.ts index bb5549a86..a5d22bb82 100644 --- a/packages/compiler/test/replacer-params.test.ts +++ b/packages/compiler/test/replacer-params.test.ts @@ -69,6 +69,8 @@ console.log(text.replace(/(a)|(b)/g, (m, a, b) => \`\${m}\${a}\${b}\`)); console.log(text.replace(/(a)(b)?/g, (m: string, a: string, b: string | undefined, at: number) => m + a + (b ?? "") + at)); const half = (n: number): number => n / 2; console.log(text.replace(/b/g, (m, at) => String(half(at)))); +console.log(text.replace(/b/g, (m, at) => String(parseFloat(at) + parseInt(at)))); +console.log(text.replace(/(\\d)/g, (m, d) => String(parseInt("11", d)))); console.log(text.replace(/(a)(b)(c)/g, (m, a, b, c, at, all, extra) => m + a + b + c + at + all + extra)); `); expect(replacerCalls(llvm)).toEqual({ @@ -76,7 +78,10 @@ console.log(text.replace(/(a)(b)(c)/g, (m, a, b, c, at, all, extra) => m + a + b // two captures a match can leave unset (bits 1 and 2); // \`string | undefined\` at capture 2 (bit 8 + 2); an offset passed to a // number parameter keeps its checked-dynamic box (bit 1). - direct: ["2:0", "2:-2147483648", "3:6", "4:1024", "2:2"], + // parseFloat and parseInt take text, and parseInt's radix is a + // number: the offset passed as text and the capture passed as the + // radix stay checked-dynamic (bit 1), so both calls compile. + direct: ["2:0", "2:-2147483648", "3:6", "4:1024", "2:2", "2:2", "2:2"], // Seven parameters exceed the direct calls' limit. boxed: 1, }); diff --git a/tests/corpus/8345-replace-callback-untyped-params.ts b/tests/corpus/8345-replace-callback-untyped-params.ts index 31d3a038b..18ddb3389 100644 --- a/tests/corpus/8345-replace-callback-untyped-params.ts +++ b/tests/corpus/8345-replace-callback-untyped-params.ts @@ -51,6 +51,11 @@ console.log("ab".replace(/(?a)|(?b)/g, (m, a, b, at, all, groups) => `${m} console.log("ab".replace(/b/, (m, at, all, extra, more) => `${m}:${at}:${all}:${extra}:${more}`)); console.log("ab".replace(/(a)(b)/, (m, a, b, at, all, extra, more) => `${m}${a}${b}${at}${all}${extra}${more}`)); +// An offset passed to parseFloat converts with ToString, and captures +// passed to parseInt and parseFloat are their text. +console.log("a1b22".replace(/\d+/g, (m, at) => `<${parseFloat(at) + parseInt(m, at + 10)}>`)); +console.log("1.5 2e1".replace(/(\S+)/g, (_m, d) => String(parseFloat(d) + parseInt(d) + Number(d)))); + // Results that are not strings convert with ToString. const counts: Record = { a: 1, b: true, c: null, d: undefined, e: { toString: () => "E" } }; console.log("a b c d e".replace(/\w/g, (m) => counts[m]));