diff --git a/packages/compiler/src/frontend/lowering/builtins/filesystem.ts b/packages/compiler/src/frontend/lowering/builtins/filesystem.ts index 3b32fa29b..d8272c795 100644 --- a/packages/compiler/src/frontend/lowering/builtins/filesystem.ts +++ b/packages/compiler/src/frontend/lowering/builtins/filesystem.ts @@ -1351,9 +1351,16 @@ export function lowerFsReaddirTypesCall( } } if (recursive !== null) { - const flag = lowerer.typeOf(recursive); + // Only the `true`/`false` keywords fold; any other expression, even one + // typed as a literal, evaluates at run time for its effects. + let flag = recursive; + while (ts.isParenthesizedExpression(flag)) flag = flag.expression; const literal = - flag.flags & ts.TypeFlags.BooleanLiteral ? lowerer.checker.typeToString(flag) : null; + flag.kind === ts.SyntaxKind.TrueKeyword + ? "true" + : flag.kind === ts.SyntaxKind.FalseKeyword + ? "false" + : null; if (sawWithFileTypes && literal !== "false") { lowerer.noLowering( `${operation} with withFileTypes and recursive`, diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 3f3f6ea33..597ddb9da 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -6011,12 +6011,19 @@ export class Lowerer { if (!primitive(e.left) || !primitive(e.right)) return null; // A missing read is undefined, and `+` yields a number only when // neither operand is a string: an operand that is always a string - // (`"x-" + PAD[i]`) makes the result a string. A read, or a sum or - // call whose result may itself be NaN, is not always a string. - const definitelyString = (part: ts.Expression): boolean => - !stringRead(part) && - this.mapTypeOf(this.typeOf(part))?.kind === "string" && - optionalPrimitiveResultType(part) === null; + // (`"x-" + PAD[i]`) makes the result a string. Only literals, + // templates and sums with such an operand are always strings; a + // binding or call can hold a missing read. + const definitelyString = (part: ts.Expression): boolean => { + const p = peel(part); + if (ts.isStringLiteral(p) || ts.isNoSubstitutionTemplateLiteral(p)) return true; + if (ts.isTemplateExpression(p)) return true; + return ( + ts.isBinaryExpression(p) && + p.operatorToken.kind === ts.SyntaxKind.PlusToken && + (definitelyString(p.left) || definitelyString(p.right)) + ); + }; if (definitelyString(e.left) || definitelyString(e.right)) return null; return { kind: "union", unionId: this.unions.intern([F64, STRING]) }; }; diff --git a/tests/corpus/fs-readdir-recursive.ts b/tests/corpus/fs-readdir-recursive.ts index 310cd3b52..39fd7f5cc 100644 --- a/tests/corpus/fs-readdir-recursive.ts +++ b/tests/corpus/fs-readdir-recursive.ts @@ -27,6 +27,22 @@ for (const flag of [false, true]) { } console.log(readdirSync(join(root, "empty"), { recursive: true }).length); +// A flag expression typed `true` or `false` still evaluates, once. +let probes = 0; +function yes(): true { + probes++; + return true; +} +function no(): false { + probes++; + return false; +} +console.log( + readdirSync(join(root, "b"), { recursive: yes() }).length, + readdirSync(join(root, "b"), { recursive: no() }).length, + probes, +); + try { readdirSync(join(root, "missing"), { recursive: true }); } catch (e) { diff --git a/tests/corpus/string-plus-element-read-return.ts b/tests/corpus/string-plus-element-read-return.ts index f4051f615..ec07fa3d5 100644 --- a/tests/corpus/string-plus-element-read-return.ts +++ b/tests/corpus/string-plus-element-read-return.ts @@ -25,3 +25,18 @@ function pair(i: number, j: number): string { } const both = pair(8, 9); console.log(pair(0, 1), typeof both, String(both)); + +// A missing element held in a binding or returned by a function is undefined, +// so two of them still add to NaN. +function at(i: number): string { + return PAD[i]; +} +function viaBinding(i: number, j: number): string { + const head = PAD[i]; + return head + PAD[j]; +} +function viaCall(i: number, j: number): string { + return at(i) + PAD[j]; +} +console.log(viaBinding(0, 1), String(viaBinding(5, 6)), typeof viaBinding(5, 6)); +console.log(viaCall(0, 2), String(viaCall(5, 6)), viaBinding(0, 9));