@chenglou/freerange 0.0.3 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chenglou/freerange",
3
- "version": "0.0.3",
3
+ "version": "0.0.4",
4
4
  "description": "Static numeric range analysis for TypeScript",
5
5
  "license": "MIT",
6
6
  "repository": {
package/src/audit.ts CHANGED
@@ -415,10 +415,8 @@ function guidesForUnsupportedReason(reason: UnsupportedReason): RefactorGuideID[
415
415
  case 'computedPropertyName':
416
416
  case 'objectSpread':
417
417
  case 'asyncOrGeneratorFunction':
418
- case 'typePredicate':
419
418
  case 'protoProperty':
420
419
  case 'enumMemberRead':
421
- case 'prototypeMemberRead':
422
420
  case 'binaryOperator':
423
421
  case 'callWithFewerArguments':
424
422
  case 'callWithMoreArguments':
package/src/ir/program.ts CHANGED
@@ -97,11 +97,8 @@ export type UnsupportedReason =
97
97
  | {kind: 'computedPropertyName'}
98
98
  | {kind: 'objectSpread'}
99
99
  | {kind: 'asyncOrGeneratorFunction'}
100
- | {kind: 'typePredicate'}
101
100
  | {kind: 'protoProperty'}
102
101
  | {kind: 'enumMemberRead'}
103
- // point.toString and friends: a prototype member the record value cannot answer.
104
- | {kind: 'prototypeMemberRead'; property: string}
105
102
  // e.g. '**', '>>', 'instanceof' in value position — the operators arithmetic and
106
103
  // comparison lowering do not claim ('%' and '??' lower now and no longer land here)
107
104
  | {kind: 'binaryOperator'; operator: string}
@@ -31,6 +31,7 @@ export type TopLevelFunction = TopLevelFunctionUnit & {
31
31
  export type FunctionContext = {
32
32
  sourceFile: ts.SourceFile
33
33
  checker: ts.TypeChecker
34
+ program: ts.Program
34
35
  functionsBySymbol: Map<ts.Symbol, TopLevelFunction>
35
36
  moduleBindingsBySymbol: Map<ts.Symbol, ModuleBindingID>
36
37
  staticAnnotations: Map<ts.CallExpression, StaticAnnotation>
@@ -60,6 +61,7 @@ export type LoopTarget = {
60
61
  export function createFunctionContext(
61
62
  sourceFile: ts.SourceFile,
62
63
  checker: ts.TypeChecker,
64
+ program: ts.Program,
63
65
  functionsBySymbol: Map<ts.Symbol, TopLevelFunction>,
64
66
  moduleBindingsBySymbol: Map<ts.Symbol, ModuleBindingID>,
65
67
  sites: SourceSpan[],
@@ -70,6 +72,7 @@ export function createFunctionContext(
70
72
  return {
71
73
  sourceFile,
72
74
  checker,
75
+ program,
73
76
  functionsBySymbol,
74
77
  moduleBindingsBySymbol,
75
78
  staticAnnotations: new Map(staticAnnotations.map(annotation => [annotation.call, annotation])),
@@ -2,7 +2,12 @@ import * as ts from 'typescript'
2
2
  import type {ValueID} from '../ir/ids.ts'
3
3
  import type {ComparisonOperator, InstructionIR} from '../ir/instructions.ts'
4
4
  import type {StaticAssertionProblem} from '../ir/program.ts'
5
- import {declaredOnlyInDeclarationFiles, platformFact} from './platform.ts'
5
+ import {numberConstituent} from './numeric-intersection.ts'
6
+ import {
7
+ declaredOnlyInDeclarationFiles,
8
+ hasDefaultLibraryDeclaration,
9
+ platformFact,
10
+ } from './platform.ts'
6
11
  import {
7
12
  addInstruction,
8
13
  addInstructionAtSite,
@@ -152,7 +157,7 @@ export function lowerExpression(expression: ts.Expression, context: FunctionCont
152
157
  if (ts.isNumericLiteral(negated)) {
153
158
  return addInstruction(context, current, {kind: 'constant', value: -Number(negated.text)})
154
159
  }
155
- if (isGlobalInfinity(negated, context.checker)) {
160
+ if (isStandardGlobal(negated, 'Infinity', context)) {
156
161
  return addInstruction(context, current, {kind: 'constant', value: Number.NEGATIVE_INFINITY})
157
162
  }
158
163
  const zero = addInstruction(context, current, {kind: 'constant', value: 0})
@@ -346,7 +351,8 @@ export function lowerExpression(expression: ts.Expression, context: FunctionCont
346
351
  // `el instanceof HTMLDivElement` on a carried value: no narrowing (the analyzer does
347
352
  // not model classes), but the check itself is an effect-free operator, so it answers
348
353
  // unknown and both branches analyze — the function's other paths survive. The
349
- // declaration-file check is the same shadowing defense Math uses.
354
+ // A declaration-file constructor is opaque but safe to test; a local constructor is
355
+ // runtime code outside the accepted subset.
350
356
  if (current.operatorToken.kind === ts.SyntaxKind.InstanceOfKeyword
351
357
  && ts.isIdentifier(current.right)
352
358
  && declaredOnlyInDeclarationFiles(context.checker.getSymbolAtLocation(current.right))) {
@@ -394,10 +400,11 @@ export function lowerExpression(expression: ts.Expression, context: FunctionCont
394
400
  if (staticAnnotation != null) return lowerStaticAnnotation(staticAnnotation, context)
395
401
  if (ts.isIdentifier(current.expression)) {
396
402
  // Global parseFloat / parseInt / Number(x): honest NaN-carrying results, like their
397
- // Number.* spellings below (the declaration-file check defends against shadowing).
403
+ // Number.* spellings below. Standard-library identity defends against shadows and
404
+ // project declarations that merely reuse a built-in name.
398
405
  const globalName = current.expression.text
399
406
  if ((globalName === 'parseFloat' || globalName === 'parseInt' || globalName === 'Number')
400
- && declaredOnlyInDeclarationFiles(context.checker.getSymbolAtLocation(current.expression))) {
407
+ && isStandardGlobal(current.expression, globalName, context)) {
401
408
  for (const argument of current.arguments) lowerExpression(argument, context)
402
409
  return addInstruction(context, current, {kind: 'parsedNumber', integer: globalName === 'parseInt'})
403
410
  }
@@ -463,12 +470,14 @@ export function lowerExpression(expression: ts.Expression, context: FunctionCont
463
470
  })
464
471
  }
465
472
  if (ts.isPropertyAccessExpression(current.expression)) {
466
- const platformCall = current.arguments.length === 0 ? platformFact(current.expression, true, context.checker) : null
473
+ const platformCall = current.arguments.length === 0
474
+ ? platformFact(current.expression, true, context.checker, context.program)
475
+ : null
467
476
  if (platformCall != null) {
468
477
  return addInstruction(context, current, {kind: 'platformValue', ...platformCall})
469
478
  }
470
479
  const method = current.expression.name.text
471
- const standardMath = isStandardMathObject(current.expression.expression, context.checker)
480
+ const standardMath = isStandardGlobal(current.expression.expression, 'Math', context)
472
481
  if (standardMath && method === 'floor' && current.arguments.length === 1) {
473
482
  requireNumberType(current.arguments[0]!, context.checker)
474
483
  const value = lowerExpression(current.arguments[0]!, context)
@@ -493,7 +502,7 @@ export function lowerExpression(expression: ts.Expression, context: FunctionCont
493
502
  // Number.isInteger / Number.isFinite: predicate checks whose branches narrow — the
494
503
  // missing halves of the bounds-check idiom (`i >= 0 && i < arr.length` proves the
495
504
  // range; Number.isInteger(i) proves the read hits an element rather than arr[1.5]).
496
- const standardNumber = isStandardNumberObject(current.expression.expression, context.checker)
505
+ const standardNumber = isStandardGlobal(current.expression.expression, 'Number', context)
497
506
  // Number.parseFloat / Number.parseInt: honest NaN sources — the result is any
498
507
  // number including NaN, and the isFinite/isNaN/isInteger narrowing downstream is
499
508
  // exactly what launders it. Arguments still lower (opaque strings carry).
@@ -522,7 +531,7 @@ export function lowerExpression(expression: ts.Expression, context: FunctionCont
522
531
  }
523
532
  }
524
533
  if (ts.isPropertyAccessExpression(current)) {
525
- const platform = platformFact(current, false, context.checker)
534
+ const platform = platformFact(current, false, context.checker, context.program)
526
535
  if (platform != null) {
527
536
  return addInstruction(context, current, {kind: 'platformValue', ...platform})
528
537
  }
@@ -597,15 +606,15 @@ function lowerStaticAnnotation(annotation: StaticAnnotation, context: FunctionCo
597
606
  if (requirement == null) {
598
607
  throw unsupported(condition, {
599
608
  kind: 'staticAssertionForm',
600
- problem: staticAssertionProblem(condition, context.checker, 'callerRequirement'),
609
+ problem: staticAssertionProblem(condition, context, 'callerRequirement'),
601
610
  })
602
611
  }
603
612
  value = lowerWrittenRequirement(requirement, condition, context)
604
613
  } else {
605
- if (!supportedWrittenAssertion(condition, context.checker)) {
614
+ if (!supportedWrittenAssertion(condition, context)) {
606
615
  throw unsupported(condition, {
607
616
  kind: 'staticAssertionForm',
608
- problem: staticAssertionProblem(condition, context.checker, 'directCheck'),
617
+ problem: staticAssertionProblem(condition, context, 'directCheck'),
609
618
  })
610
619
  }
611
620
  value = lowerExpression(condition, context)
@@ -642,7 +651,7 @@ function writtenRequirement(condition: ts.Expression, context: FunctionContext):
642
651
  if (ts.isCallExpression(current) && current.questionDotToken == null
643
652
  && current.arguments.length === 1 && ts.isPropertyAccessExpression(current.expression)
644
653
  && current.expression.questionDotToken == null
645
- && isStandardNumberObject(current.expression.expression, context.checker)
654
+ && isStandardGlobal(current.expression.expression, 'Number', context)
646
655
  && (current.expression.name.text === 'isInteger' || current.expression.name.text === 'isFinite')) {
647
656
  const argument = current.arguments[0]!
648
657
  const value = staticRequirementParameterPathValue(argument, context)
@@ -744,13 +753,14 @@ function lowerWrittenRequirement(
744
753
  // calculation is written and checked as normal code, then the assertion names its result.
745
754
  // This gives agents one syntax boundary instead of exposing whichever instructions happen
746
755
  // to be removable after general expression lowering.
747
- function supportedWrittenAssertion(condition: ts.Expression, checker: ts.TypeChecker): boolean {
756
+ function supportedWrittenAssertion(condition: ts.Expression, context: FunctionContext): boolean {
748
757
  const current = unwrapParentheses(condition)
749
758
  if (ts.isBinaryExpression(current) && staticAssertionComparison(current.operatorToken.kind)) {
750
- return staticAssertionNumericAtom(current.left, checker) && staticAssertionNumericAtom(current.right, checker)
759
+ return staticAssertionNumericAtom(current.left, context.checker)
760
+ && staticAssertionNumericAtom(current.right, context.checker)
751
761
  }
752
- const operand = staticNumberCheckOperand(current, checker)
753
- return operand != null && staticAssertionNumericAtom(operand, checker)
762
+ const operand = staticNumberCheckOperand(current, context)
763
+ return operand != null && staticAssertionNumericAtom(operand, context.checker)
754
764
  }
755
765
 
756
766
  function staticAssertionNumericAtom(expression: ts.Expression, checker: ts.TypeChecker): boolean {
@@ -802,25 +812,25 @@ function staticAssertionAtom(expression: ts.Expression): boolean {
802
812
 
803
813
  function staticAssertionProblem(
804
814
  condition: ts.Expression,
805
- checker: ts.TypeChecker,
815
+ context: FunctionContext,
806
816
  fallback: 'directCheck' | 'callerRequirement',
807
817
  ): StaticAssertionProblem {
808
818
  const current = unwrapParentheses(condition)
809
819
  if (ts.isBinaryExpression(current) && staticAssertionComparison(current.operatorToken.kind)) {
810
820
  return staticAssertionAtomProblem(current.left) ?? staticAssertionAtomProblem(current.right) ?? fallback
811
821
  }
812
- const operand = staticNumberCheckOperand(current, checker)
822
+ const operand = staticNumberCheckOperand(current, context)
813
823
  if (operand != null) return staticAssertionAtomProblem(operand) ?? fallback
814
824
  if (ts.isCallExpression(current)) return 'functionCall'
815
825
  return 'directCheck'
816
826
  }
817
827
 
818
- function staticNumberCheckOperand(expression: ts.Expression, checker: ts.TypeChecker): ts.Expression | null {
828
+ function staticNumberCheckOperand(expression: ts.Expression, context: FunctionContext): ts.Expression | null {
819
829
  if (!ts.isCallExpression(expression) || expression.questionDotToken != null
820
830
  || expression.arguments.length !== 1 || !ts.isPropertyAccessExpression(expression.expression)
821
831
  || expression.expression.questionDotToken != null) return null
822
832
  const callee = expression.expression
823
- return isStandardNumberObject(callee.expression, checker)
833
+ return isStandardGlobal(callee.expression, 'Number', context)
824
834
  && (callee.name.text === 'isInteger' || callee.name.text === 'isFinite' || callee.name.text === 'isNaN')
825
835
  ? expression.arguments[0]!
826
836
  : null
@@ -1150,7 +1160,7 @@ export function valueKind(type: ts.Type, checker: ts.TypeChecker, depth = 0): Va
1150
1160
  }
1151
1161
 
1152
1162
  function valueKindUncached(type: ts.Type, checker: ts.TypeChecker, depth: number): ValueKindResult {
1153
- if ((type.flags & ts.TypeFlags.NumberLike) !== 0) return 'number'
1163
+ if (numberConstituent(type) != null) return 'number'
1154
1164
  if ((type.flags & ts.TypeFlags.BooleanLike) !== 0) return 'boolean'
1155
1165
  // Strings are carried without claims: a label or id must not reject the numeric
1156
1166
  // contract of the function around it.
@@ -1440,22 +1450,6 @@ export function requireBooleanCondition(node: ts.Node, checker: ts.TypeChecker):
1440
1450
  }
1441
1451
 
1442
1452
  function requireAccessedPropertyKind(access: ts.PropertyAccessExpression, checker: ts.TypeChecker): void {
1443
- // An optional property reads as its maybe-undefined value: declared kinds wrap it in
1444
- // the undefined sentinel and object literals fill omitted ones explicitly, so a record
1445
- // value always carries every property its static type declares — there is always
1446
- // something honest to read.
1447
- const receiverType = checker.getTypeAtLocation(access.expression)
1448
- // For an optional read the receiver includes the missing sentinels; the property lives
1449
- // on the non-missing part, which getNonNullableType strips to.
1450
- const presentType = access.questionDotToken != null ? checker.getNonNullableType(receiverType) : receiverType
1451
- const property = checker.getPropertyOfType(presentType, access.name.text)
1452
- // point.toString type-checks on every object literal, but the record value carries only
1453
- // its own properties — an inherited prototype member has no honest answer. The
1454
- // ownership test is the .d.ts rule: a property symbol declared only in declaration
1455
- // files was not written by the project, and on a project record that means prototype.
1456
- if (valueKind(presentType, checker) === 'object' && property != null && declaredOnlyInDeclarationFiles(property)) {
1457
- throw unsupported(access, {kind: 'prototypeMemberRead', property: access.name.text})
1458
- }
1459
1453
  const type = checker.getTypeAtLocation(access)
1460
1454
  if (valueKind(type, checker) != null) return
1461
1455
  throw unsupported(access, {kind: 'valueType', typeText: checker.typeToString(type)})
@@ -1466,14 +1460,12 @@ function resolvedSymbol(symbol: ts.Symbol | undefined, checker: ts.TypeChecker):
1466
1460
  return (symbol.flags & ts.SymbolFlags.Alias) === 0 ? symbol : checker.getAliasedSymbol(symbol)
1467
1461
  }
1468
1462
 
1469
- function isStandardMathObject(expression: ts.Expression, checker: ts.TypeChecker): boolean {
1470
- if (!ts.isIdentifier(expression) || expression.text !== 'Math') return false
1471
- return declaredOnlyInDeclarationFiles(checker.getSymbolAtLocation(expression))
1472
- }
1473
-
1474
- function isStandardNumberObject(expression: ts.Expression, checker: ts.TypeChecker): boolean {
1475
- if (!ts.isIdentifier(expression) || expression.text !== 'Number') return false
1476
- return declaredOnlyInDeclarationFiles(checker.getSymbolAtLocation(expression))
1463
+ function isStandardGlobal(expression: ts.Expression, name: string, context: FunctionContext): boolean {
1464
+ if (!ts.isIdentifier(expression) || expression.text !== name) return false
1465
+ return hasDefaultLibraryDeclaration(
1466
+ context.checker.getSymbolAtLocation(expression),
1467
+ context.program,
1468
+ )
1477
1469
  }
1478
1470
 
1479
1471
  function lowerElementAccess(access: ts.ElementAccessExpression, asserted: boolean, context: FunctionContext): ValueID {
@@ -1636,7 +1628,10 @@ function missingSentinelCheck(expression: ts.BinaryExpression, context: Function
1636
1628
  const missing = ts.TypeFlags.Null | ts.TypeFlags.Undefined
1637
1629
  const members = operandType.isUnion() ? operandType.types : [operandType]
1638
1630
  const restMatches = members.every(member =>
1639
- (member.flags & missing) !== 0 || (member.flags & typeofSide.flags) !== 0)
1631
+ (member.flags & missing) !== 0
1632
+ || (typeofSide.flags === ts.TypeFlags.NumberLike
1633
+ ? valueKind(member, context.checker) === 'number'
1634
+ : (member.flags & typeofSide.flags) !== 0))
1640
1635
  && members.some(member => (member.flags & missing) === 0)
1641
1636
  const value = lowerExpression(typeofSide.operand.expression, context)
1642
1637
  if (restMatches) {
@@ -1670,22 +1665,17 @@ function missingSentinelCheck(expression: ts.BinaryExpression, context: Function
1670
1665
  })
1671
1666
  }
1672
1667
 
1673
- function isGlobalInfinity(expression: ts.Expression, checker: ts.TypeChecker): boolean {
1674
- if (!ts.isIdentifier(expression) || expression.text !== 'Infinity') return false
1675
- return declaredOnlyInDeclarationFiles(checker.getSymbolAtLocation(expression))
1676
- }
1677
-
1678
1668
  // Resolves an identifier read: a function-local binding first, then a module binding
1679
1669
  // (which reads the binding's slot), then the global `Infinity` as an exact constant,
1680
1670
  // else the identifier is unknown. A local or module binding named Infinity has a
1681
- // different symbol and wins above; the global check is the same declaration-file
1682
- // defense the Math and platform dispatches use.
1671
+ // different symbol and wins above; only TypeScript's standard Infinity receives the
1672
+ // exact constant.
1683
1673
  function identifierValue(symbol: ts.Symbol, node: ts.Identifier, context: FunctionContext): ValueID {
1684
1674
  const local = context.bindings.get(symbol)
1685
1675
  if (local != null) return local
1686
1676
  const binding = context.moduleBindingsBySymbol.get(symbol)
1687
1677
  if (binding != null) return addInstruction(context, node, {kind: 'moduleRead', binding})
1688
- if (isGlobalInfinity(node, context.checker)) {
1678
+ if (isStandardGlobal(node, 'Infinity', context)) {
1689
1679
  return addInstruction(context, node, {kind: 'constant', value: Number.POSITIVE_INFINITY})
1690
1680
  }
1691
1681
  if (isUndefinedGlobal(node, context.checker)) {
@@ -19,6 +19,7 @@ import {declaredOnlyInDeclarationFiles} from './platform.ts'
19
19
  import {addInstruction, addSite, createFunctionContext, LoweringStop, restoreLowering, sealBlocks, snapshotLowering, terminate, type FunctionContext, type TopLevelFunction} from './context.ts'
20
20
  import {lowerExpression, nonMissingUnionMembers, tagLiteralValues, taggedUnionProperty, valueKind} from './expression.ts'
21
21
  import {directFunctionExpression} from './function-unit.ts'
22
+ import {numberConstituent} from './numeric-intersection.ts'
22
23
  import {lowerStatement} from './statements.ts'
23
24
 
24
25
  export type ModuleScan = {
@@ -197,11 +198,12 @@ function demoteModuleWritesInNode(
197
198
  export function lowerModuleInitializer(
198
199
  sourceFile: ts.SourceFile,
199
200
  checker: ts.TypeChecker,
201
+ program: ts.Program,
200
202
  functionsBySymbol: Map<ts.Symbol, TopLevelFunction>,
201
203
  scan: ModuleScan,
202
204
  sites: SourceSpan[],
203
205
  ): {initializer: FunctionIR; skips: InitializerSkip[]} {
204
- const context = createFunctionContext(sourceFile, checker, functionsBySymbol, scan.bindingsBySymbol, sites)
206
+ const context = createFunctionContext(sourceFile, checker, program, functionsBySymbol, scan.bindingsBySymbol, sites)
205
207
  const skips: InitializerSkip[] = []
206
208
  const statements = sourceFile.statements
207
209
  // Each statement gets its own catch: an unsupported one is skipped — its half-lowered
@@ -682,14 +684,15 @@ function declaredKindUncached(type: ts.Type, checker: ts.TypeChecker, seen: ts.T
682
684
 
683
685
  function numericLiteralInterval(type: ts.Type): DeclaredNumberInterval | 'nonFinite' | null {
684
686
  const members = type.isUnion() ? type.types : [type]
685
- if (!members.every(member =>
686
- (member.flags & ts.TypeFlags.NumberLiteral) !== 0
687
- && (member.flags & ts.TypeFlags.EnumLiteral) === 0)) return null
688
687
  let lower = Infinity
689
688
  let upper = -Infinity
690
689
  let integer = true
691
690
  for (const member of members) {
692
- const value = (member as ts.NumberLiteralType).value
691
+ const number = numberConstituent(member)
692
+ if (number == null
693
+ || (number.flags & ts.TypeFlags.NumberLiteral) === 0
694
+ || (number.flags & ts.TypeFlags.EnumLiteral) !== 0) return null
695
+ const value = (number as ts.NumberLiteralType).value
693
696
  if (!Number.isFinite(value)) return 'nonFinite'
694
697
  lower = Math.min(lower, value)
695
698
  upper = Math.max(upper, value)
@@ -0,0 +1,15 @@
1
+ import * as ts from 'typescript'
2
+
3
+ // TypeScript represents numeric brands as a number intersected with object metadata.
4
+ // Freerange uses the number and ignores the metadata; reading that metadata still stops
5
+ // because the runtime value is modeled as a number, not a record.
6
+ export function numberConstituent(type: ts.Type): ts.Type | null {
7
+ if ((type.flags & ts.TypeFlags.NumberLike) !== 0) return type
8
+ if (!type.isIntersection()) return null
9
+ const numberTypes = type.types.filter(member => (member.flags & ts.TypeFlags.NumberLike) !== 0)
10
+ if (numberTypes.length !== 1) return null
11
+ for (const member of type.types) {
12
+ if (member !== numberTypes[0] && (member.flags & ts.TypeFlags.Object) === 0) return null
13
+ }
14
+ return numberTypes[0]!
15
+ }
@@ -29,6 +29,7 @@ type PlatformEntry = {
29
29
  }
30
30
 
31
31
  const anyFinite = {lower: -Number.MAX_VALUE, upper: Number.MAX_VALUE}
32
+ const maximumDateTime = 8_640_000_000_000_000
32
33
 
33
34
  const catalog: PlatformEntry[] = [
34
35
  // Element layout sizes are nonnegative integers (the spec rounds them).
@@ -43,19 +44,23 @@ const catalog: PlatformEntry[] = [
43
44
  {path: ['window', 'scrollY'], call: false, fact: {...anyFinite, integer: false}},
44
45
  {path: ['document', 'body', 'scrollTop'], call: false, fact: {...anyFinite, integer: false}},
45
46
  {path: ['document', 'body', 'scrollLeft'], call: false, fact: {...anyFinite, integer: false}},
46
- // Monotonic clocks: finite, nonnegative, fractional (performance.now has sub-millisecond
47
- // resolution); Date.now is an integer count of milliseconds.
47
+ // performance.now is monotonic, finite, nonnegative, and fractional. Date.now is an
48
+ // integer UTC time value and may be negative for a clock set before the Unix epoch.
48
49
  {path: ['performance', 'now'], call: true, fact: {lower: 0, upper: Number.MAX_VALUE, integer: false}},
49
- {path: ['Date', 'now'], call: true, fact: {lower: 0, upper: Number.MAX_VALUE, integer: true}},
50
+ {path: ['Date', 'now'], call: true, fact: {lower: -maximumDateTime, upper: maximumDateTime, integer: true}},
50
51
  // Math.random returns one of JavaScript's discrete floating-point numbers below 1.
51
52
  // Writing the predecessor of 1 keeps the exact closed range without adding open bounds.
52
53
  {path: ['Math', 'random'], call: true, fact: {lower: 0, upper: 0.9999999999999999, integer: false}},
53
54
  ]
54
55
 
55
- // Matches a property chain rooted at a global whose symbol resolves into a declaration
56
- // file the same shadowing defense the Math dispatch uses, so a local variable named
57
- // `document` never matches.
58
- export function platformFact(expression: ts.Expression, call: boolean, checker: ts.TypeChecker): PlatformFact | null {
56
+ // Matches a property chain rooted at a global from TypeScript's standard library. A local
57
+ // variable or a project-supplied ambient declaration with the same name never matches.
58
+ export function platformFact(
59
+ expression: ts.Expression,
60
+ call: boolean,
61
+ checker: ts.TypeChecker,
62
+ program: ts.Program,
63
+ ): PlatformFact | null {
59
64
  const parts: string[] = []
60
65
  let current: ts.Expression = expression
61
66
  while (ts.isPropertyAccessExpression(current)) {
@@ -69,14 +74,22 @@ export function platformFact(expression: ts.Expression, call: boolean, checker:
69
74
  && candidate.path.length === parts.length
70
75
  && candidate.path.every((segment, index) => segment === parts[index]))
71
76
  if (entry == null) return null
72
- if (!declaredOnlyInDeclarationFiles(checker.getSymbolAtLocation(current))) return null
77
+ if (!hasDefaultLibraryDeclaration(checker.getSymbolAtLocation(current), program)) return null
73
78
  return entry.fact
74
79
  }
75
80
 
76
- // The shadowing defense shared by every trusted-global dispatch (this catalog, `Math`,
77
- // `Infinity`): the identifier counts as the real global only when every declaration of its
78
- // symbol lives in a declaration file, so a local or module binding of the same name never
79
- // matches.
81
+ // A standard-library declaration anchors the symbol to the real JavaScript or browser
82
+ // global. Additional project declarations may augment that symbol; a lookalike symbol
83
+ // declared only by the project does not receive Freerange's built-in behavior.
84
+ export function hasDefaultLibraryDeclaration(
85
+ symbol: ts.Symbol | undefined,
86
+ program: ts.Program,
87
+ ): boolean {
88
+ const declarations = symbol?.declarations
89
+ if (declarations == null || declarations.length === 0) return false
90
+ return declarations.some(declaration => program.isSourceFileDefaultLibrary(declaration.getSourceFile()))
91
+ }
92
+
80
93
  export function declaredOnlyInDeclarationFiles(symbol: ts.Symbol | undefined): boolean {
81
94
  const declarations = symbol?.declarations
82
95
  if (declarations == null || declarations.length === 0) return false
@@ -12,7 +12,8 @@ import {scanStaticAnnotations, type StaticAnnotation} from './static-intrinsics.
12
12
  import {lowerStatements} from './statements.ts'
13
13
 
14
14
  export function lowerSource(checked: CheckedSource, baseDirectory: string = process.cwd()): ProgramIR {
15
- const {sourceFile, checker} = checked
15
+ const {sourceFile, program} = checked
16
+ const checker = program.getTypeChecker()
16
17
  const declarations = topLevelFunctionUnits(sourceFile)
17
18
  const staticScan = scanStaticAnnotations(sourceFile, declarations, checker)
18
19
  const recordStaticAnnotationIssues = (sites: SourceSpan[]): ProgramIR['staticAnnotationIssues'] =>
@@ -91,7 +92,7 @@ export function lowerSource(checked: CheckedSource, baseDirectory: string = proc
91
92
  // A failed function lowering discards the half-built FunctionContext wholesale; only
92
93
  // the name, annotation presence, offending node's site, and tagged reason survive.
93
94
  try {
94
- functions.push(lowerFunction(declaration, staticAnnotations, sourceFile, checker, functionsBySymbol, scan, sites))
95
+ functions.push(lowerFunction(declaration, staticAnnotations, sourceFile, checker, program, functionsBySymbol, scan, sites))
95
96
  } catch (error) {
96
97
  if (!(error instanceof LoweringStop)) throw error
97
98
  sites.push(nodeSpan(sourceFile, error.node))
@@ -104,7 +105,7 @@ export function lowerSource(checked: CheckedSource, baseDirectory: string = proc
104
105
  })
105
106
  }
106
107
  }
107
- const {initializer, skips} = lowerModuleInitializer(sourceFile, checker, functionsBySymbol, scan, sites)
108
+ const {initializer, skips} = lowerModuleInitializer(sourceFile, checker, program, functionsBySymbol, scan, sites)
108
109
  return {
109
110
  file: sourceFile.fileName,
110
111
  baseDirectory,
@@ -123,6 +124,7 @@ function lowerFunction(
123
124
  staticAnnotations: StaticAnnotation[],
124
125
  sourceFile: ts.SourceFile,
125
126
  checker: ts.TypeChecker,
127
+ program: ts.Program,
126
128
  functionsBySymbol: Map<ts.Symbol, TopLevelFunction>,
127
129
  scan: ModuleScan,
128
130
  sites: SourceSpan[],
@@ -148,12 +150,6 @@ function lowerFunction(
148
150
  kind: unit.initializer == null ? 'functionWithoutSignature' : 'constFunctionSignature',
149
151
  })
150
152
  }
151
- // A type predicate (`shape is Circle`, `asserts x`) is the checker taking the author's
152
- // word: callers' narrowing then exposes properties the analysis cannot confirm the
153
- // value carries. Rejecting the declaring function stops every caller at the call.
154
- if (checker.getTypePredicateOfSignature(signature) != null) {
155
- throw unsupported(declaration, {kind: 'typePredicate'})
156
- }
157
153
  const returnType = checker.getReturnTypeOfSignature(signature)
158
154
  // `never` counts as returning nothing: the idiomatic annotation for an always-throwing
159
155
  // helper (`function fail(code: number): never`), whose paths all end in throw — the
@@ -167,6 +163,7 @@ function lowerFunction(
167
163
  const context = createFunctionContext(
168
164
  sourceFile,
169
165
  checker,
166
+ program,
170
167
  functionsBySymbol,
171
168
  scan.bindingsBySymbol,
172
169
  sites,
@@ -14,7 +14,6 @@ import {
14
14
  type MutableBlock,
15
15
  } from './context.ts'
16
16
  import {taggedUnionTagRead, identifierAssignment, lowerBranchingCondition, lowerExpression, lowerStatementExpression, requireBooleanCondition, valueKind} from './expression.ts'
17
- import {declaredOnlyInDeclarationFiles} from './platform.ts'
18
17
 
19
18
  export function lowerStatements(statements: readonly ts.Statement[], context: FunctionContext): void {
20
19
  for (const statement of statements) {
@@ -489,15 +488,8 @@ function lowerVariableDeclarationList(declarations: ts.VariableDeclarationList,
489
488
  : null
490
489
  if (property == null) throw unsupported(element, {kind: 'variableDeclarationShape'})
491
490
  const elementType = context.checker.getTypeAtLocation(element.name)
492
- const sourceType = context.checker.getTypeAtLocation(declaration.initializer)
493
- const propertySymbol = context.checker.getPropertyOfType(sourceType, property)
494
- // Optional properties destructure like any other read now that the filling
495
- // invariant guarantees the record value carries them as maybe-undefined (the old
496
- // gate here predated the optionals milestone). Prototype members stay out via
497
- // the same ownership rule property reads use.
498
- if (propertySymbol != null && declaredOnlyInDeclarationFiles(propertySymbol)) {
499
- throw unsupported(element, {kind: 'prototypeMemberRead', property})
500
- }
491
+ // Optional properties destructure like other reads because the record carries
492
+ // omitted properties as maybe-undefined values.
501
493
  if (valueKind(elementType, context.checker) == null) {
502
494
  throw unsupported(element, {kind: 'valueType', typeText: context.checker.typeToString(elementType)})
503
495
  }
package/src/project.ts CHANGED
@@ -461,7 +461,7 @@ function analyzeProjectSource(
461
461
  ): DetailedAnalysis {
462
462
  return analyzeCheckedSource({
463
463
  sourceFile: source.sourceFile,
464
- checker: source.project.program.getTypeChecker(),
464
+ program: source.project.program,
465
465
  }, reportBaseDirectory)
466
466
  }
467
467
 
@@ -918,10 +918,8 @@ export function formatUnsupportedReason(reason: UnsupportedReason): string {
918
918
  case 'computedPropertyName': return 'computed object property name'
919
919
  case 'objectSpread': return 'object spread (list every field explicitly, e.g. {gain: config.gain})'
920
920
  case 'asyncOrGeneratorFunction': return 'an async or generator function (the runtime result is a Promise or iterator, not the body\'s return value)'
921
- case 'typePredicate': return 'a type predicate (the checker takes the predicate on faith; return a plain boolean and check properties where they are read)'
922
921
  case 'protoProperty': return 'a property named __proto__ (prototype-setting syntax at runtime, not a data property)'
923
922
  case 'enumMemberRead': return 'an enum member read (replace the enum with plain module consts, e.g. const directionUp = 1)'
924
- case 'prototypeMemberRead': return `read of the inherited prototype member ${reason.property} (records carry only their own data properties)`
925
923
  case 'binaryOperator': return reason.operator === 'in'
926
924
  ? 'the `in` operator (use a distinct string or boolean tag when property presence distinguishes union variants)'
927
925
  : `binary operator ${reason.operator} (supported: + - * / %, comparisons, and boolean && || !)`
@@ -4,7 +4,7 @@ import {TypeScriptDiagnosticsError} from './diagnostics.ts'
4
4
 
5
5
  export type CheckedSource = {
6
6
  sourceFile: ts.SourceFile
7
- checker: ts.TypeChecker
7
+ program: ts.Program
8
8
  }
9
9
 
10
10
  // When no tsconfig is in scope, single-file analysis gets the recommended authoring
@@ -52,5 +52,5 @@ function checkedSource(program: ts.Program, file: string, options: ts.CompilerOp
52
52
  }
53
53
  const sourceFile = program.getSourceFile(file)
54
54
  if (sourceFile == null) throw new Error(`TypeScript did not load ${file}`)
55
- return {sourceFile, checker: program.getTypeChecker()}
55
+ return {sourceFile, program}
56
56
  }