diff --git a/packages/compiler/src/ir/integer-loops.test.ts b/packages/compiler/src/ir/integer-loops.test.ts index 75968b262..aada3b053 100644 --- a/packages/compiler/src/ir/integer-loops.test.ts +++ b/packages/compiler/src/ir/integer-loops.test.ts @@ -209,6 +209,45 @@ test("a string read at the counter is an integer consumer", () => { expect(matchIntegerCountedForLoop(s, withText)).toBeNull(); }); +test("a string length bounds a counted loop while its binding is unchanged", () => { + const withText = new Map(locals); + withText.set("s", { id: "s", name: "s", type: STRING, mutable: true }); + const text = (): IrExpr => ({ kind: "varRef", localId: "s", type: STRING, loc }); + const length: IrExpr = { + kind: "strIntrinsic", + method: "length", + receiver: text(), + args: [], + type: F64, + loc, + }; + const s = counted(length); + s.body = [ + { + kind: "exprStmt", + expr: { + kind: "strIntrinsic", + method: "charCodeAt", + receiver: text(), + args: [ref("i")], + type: F64, + loc, + }, + loc, + }, + ]; + expect(matchIntegerCountedForLoop(s, withText)?.localId).toBe("i"); + // Rebinding the string in the body changes the length the condition reads. + const rebinding = { + ...s, + body: [...s.body, { kind: "assign", localId: "s", value: text(), loc } as IrStmt], + }; + expect(matchIntegerCountedForLoop(rebinding, withText)).toBeNull(); + const boxed = new Map(withText); + boxed.set("s", { ...withText.get("s")!, boxed: true }); + expect(matchIntegerCountedForLoop(s, boxed)).toBeNull(); +}); + test("a stable array length bounds a counted loop; a resizing body keeps it a condition", () => { const ARR = arrayOf(F64); const array = (localId: string): IrExpr => ({ kind: "varRef", localId, type: ARR, loc }); diff --git a/packages/compiler/src/ir/integer-loops.ts b/packages/compiler/src/ir/integer-loops.ts index 3ba186376..9b4675685 100644 --- a/packages/compiler/src/ir/integer-loops.ts +++ b/packages/compiler/src/ir/integer-loops.ts @@ -83,6 +83,13 @@ function stableLoopValue( stableLoopValue(value.left, locals, body, invariant, counter, stableLength) && stableLoopValue(value.right, locals, body, invariant, counter, stableLength) ); + case "strIntrinsic": + // A string's length is fixed: stable while its binding is. + return ( + value.method === "length" && + value.receiver.kind === "varRef" && + stableLoopValue(value.receiver, locals, body, invariant, counter) + ); case "bytesIntrinsic": case "arrIntrinsic": return (