@chenglou/freerange 0.0.2 → 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/CHANGELOG.md +14 -0
- package/README.md +232 -66
- package/dist/fr.js +627 -480
- package/package.json +1 -1
- package/src/audit.ts +2 -3
- package/src/domain/value-identity.ts +56 -0
- package/src/engine/analyze.ts +19 -12
- package/src/engine/state.ts +23 -11
- package/src/engine/transfer.ts +78 -29
- package/src/ir/instructions.ts +7 -1
- package/src/ir/program.ts +7 -3
- package/src/lower/accept.ts +6 -2
- package/src/lower/context.ts +14 -5
- package/src/lower/expression.ts +106 -88
- package/src/lower/function-unit.ts +64 -0
- package/src/lower/module.ts +28 -9
- package/src/lower/numeric-intersection.ts +15 -0
- package/src/lower/platform.ts +28 -12
- package/src/lower/program.ts +66 -41
- package/src/lower/statements.ts +5 -18
- package/src/lower/static-intrinsics.ts +9 -8
- package/src/project.ts +3 -3
- package/src/report/index.ts +2 -2
- package/src/requirements/infer.ts +76 -25
- package/src/typescript/check.ts +2 -2
package/src/lower/expression.ts
CHANGED
|
@@ -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 {
|
|
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 (
|
|
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
|
|
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
|
|
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
|
-
&&
|
|
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
|
}
|
|
@@ -420,7 +427,10 @@ export function lowerExpression(expression: ts.Expression, context: FunctionCont
|
|
|
420
427
|
arguments_.push(lowerParameterDefault(default_, parameter.initializer, context))
|
|
421
428
|
continue
|
|
422
429
|
}
|
|
423
|
-
|
|
430
|
+
const callableParameter = callee.signature?.declaration?.parameters[index]
|
|
431
|
+
if (callableParameter != null
|
|
432
|
+
&& ts.isParameter(callableParameter)
|
|
433
|
+
&& callableParameter.questionToken != null) {
|
|
424
434
|
arguments_.push(addInstruction(context, parameter, {kind: 'nullishConstant', sentinel: 'undefined'}))
|
|
425
435
|
continue
|
|
426
436
|
}
|
|
@@ -452,15 +462,22 @@ export function lowerExpression(expression: ts.Expression, context: FunctionCont
|
|
|
452
462
|
context,
|
|
453
463
|
))
|
|
454
464
|
}
|
|
455
|
-
return addInstruction(context, current, {
|
|
465
|
+
return addInstruction(context, current, {
|
|
466
|
+
kind: 'call',
|
|
467
|
+
function: callee.id,
|
|
468
|
+
arguments: arguments_,
|
|
469
|
+
binding: callee.binding,
|
|
470
|
+
})
|
|
456
471
|
}
|
|
457
472
|
if (ts.isPropertyAccessExpression(current.expression)) {
|
|
458
|
-
const platformCall = current.arguments.length === 0
|
|
473
|
+
const platformCall = current.arguments.length === 0
|
|
474
|
+
? platformFact(current.expression, true, context.checker, context.program)
|
|
475
|
+
: null
|
|
459
476
|
if (platformCall != null) {
|
|
460
477
|
return addInstruction(context, current, {kind: 'platformValue', ...platformCall})
|
|
461
478
|
}
|
|
462
479
|
const method = current.expression.name.text
|
|
463
|
-
const standardMath =
|
|
480
|
+
const standardMath = isStandardGlobal(current.expression.expression, 'Math', context)
|
|
464
481
|
if (standardMath && method === 'floor' && current.arguments.length === 1) {
|
|
465
482
|
requireNumberType(current.arguments[0]!, context.checker)
|
|
466
483
|
const value = lowerExpression(current.arguments[0]!, context)
|
|
@@ -485,7 +502,7 @@ export function lowerExpression(expression: ts.Expression, context: FunctionCont
|
|
|
485
502
|
// Number.isInteger / Number.isFinite: predicate checks whose branches narrow — the
|
|
486
503
|
// missing halves of the bounds-check idiom (`i >= 0 && i < arr.length` proves the
|
|
487
504
|
// range; Number.isInteger(i) proves the read hits an element rather than arr[1.5]).
|
|
488
|
-
const standardNumber =
|
|
505
|
+
const standardNumber = isStandardGlobal(current.expression.expression, 'Number', context)
|
|
489
506
|
// Number.parseFloat / Number.parseInt: honest NaN sources — the result is any
|
|
490
507
|
// number including NaN, and the isFinite/isNaN/isInteger narrowing downstream is
|
|
491
508
|
// exactly what launders it. Arguments still lower (opaque strings carry).
|
|
@@ -514,7 +531,7 @@ export function lowerExpression(expression: ts.Expression, context: FunctionCont
|
|
|
514
531
|
}
|
|
515
532
|
}
|
|
516
533
|
if (ts.isPropertyAccessExpression(current)) {
|
|
517
|
-
const platform = platformFact(current, false, context.checker)
|
|
534
|
+
const platform = platformFact(current, false, context.checker, context.program)
|
|
518
535
|
if (platform != null) {
|
|
519
536
|
return addInstruction(context, current, {kind: 'platformValue', ...platform})
|
|
520
537
|
}
|
|
@@ -589,15 +606,15 @@ function lowerStaticAnnotation(annotation: StaticAnnotation, context: FunctionCo
|
|
|
589
606
|
if (requirement == null) {
|
|
590
607
|
throw unsupported(condition, {
|
|
591
608
|
kind: 'staticAssertionForm',
|
|
592
|
-
problem: staticAssertionProblem(condition, context
|
|
609
|
+
problem: staticAssertionProblem(condition, context, 'callerRequirement'),
|
|
593
610
|
})
|
|
594
611
|
}
|
|
595
612
|
value = lowerWrittenRequirement(requirement, condition, context)
|
|
596
613
|
} else {
|
|
597
|
-
if (!supportedWrittenAssertion(condition, context
|
|
614
|
+
if (!supportedWrittenAssertion(condition, context)) {
|
|
598
615
|
throw unsupported(condition, {
|
|
599
616
|
kind: 'staticAssertionForm',
|
|
600
|
-
problem: staticAssertionProblem(condition, context
|
|
617
|
+
problem: staticAssertionProblem(condition, context, 'directCheck'),
|
|
601
618
|
})
|
|
602
619
|
}
|
|
603
620
|
value = lowerExpression(condition, context)
|
|
@@ -634,7 +651,7 @@ function writtenRequirement(condition: ts.Expression, context: FunctionContext):
|
|
|
634
651
|
if (ts.isCallExpression(current) && current.questionDotToken == null
|
|
635
652
|
&& current.arguments.length === 1 && ts.isPropertyAccessExpression(current.expression)
|
|
636
653
|
&& current.expression.questionDotToken == null
|
|
637
|
-
&&
|
|
654
|
+
&& isStandardGlobal(current.expression.expression, 'Number', context)
|
|
638
655
|
&& (current.expression.name.text === 'isInteger' || current.expression.name.text === 'isFinite')) {
|
|
639
656
|
const argument = current.arguments[0]!
|
|
640
657
|
const value = staticRequirementParameterPathValue(argument, context)
|
|
@@ -736,13 +753,14 @@ function lowerWrittenRequirement(
|
|
|
736
753
|
// calculation is written and checked as normal code, then the assertion names its result.
|
|
737
754
|
// This gives agents one syntax boundary instead of exposing whichever instructions happen
|
|
738
755
|
// to be removable after general expression lowering.
|
|
739
|
-
function supportedWrittenAssertion(condition: ts.Expression,
|
|
756
|
+
function supportedWrittenAssertion(condition: ts.Expression, context: FunctionContext): boolean {
|
|
740
757
|
const current = unwrapParentheses(condition)
|
|
741
758
|
if (ts.isBinaryExpression(current) && staticAssertionComparison(current.operatorToken.kind)) {
|
|
742
|
-
return staticAssertionNumericAtom(current.left,
|
|
759
|
+
return staticAssertionNumericAtom(current.left, context.checker)
|
|
760
|
+
&& staticAssertionNumericAtom(current.right, context.checker)
|
|
743
761
|
}
|
|
744
|
-
const operand = staticNumberCheckOperand(current,
|
|
745
|
-
return operand != null && staticAssertionNumericAtom(operand, checker)
|
|
762
|
+
const operand = staticNumberCheckOperand(current, context)
|
|
763
|
+
return operand != null && staticAssertionNumericAtom(operand, context.checker)
|
|
746
764
|
}
|
|
747
765
|
|
|
748
766
|
function staticAssertionNumericAtom(expression: ts.Expression, checker: ts.TypeChecker): boolean {
|
|
@@ -794,25 +812,25 @@ function staticAssertionAtom(expression: ts.Expression): boolean {
|
|
|
794
812
|
|
|
795
813
|
function staticAssertionProblem(
|
|
796
814
|
condition: ts.Expression,
|
|
797
|
-
|
|
815
|
+
context: FunctionContext,
|
|
798
816
|
fallback: 'directCheck' | 'callerRequirement',
|
|
799
817
|
): StaticAssertionProblem {
|
|
800
818
|
const current = unwrapParentheses(condition)
|
|
801
819
|
if (ts.isBinaryExpression(current) && staticAssertionComparison(current.operatorToken.kind)) {
|
|
802
820
|
return staticAssertionAtomProblem(current.left) ?? staticAssertionAtomProblem(current.right) ?? fallback
|
|
803
821
|
}
|
|
804
|
-
const operand = staticNumberCheckOperand(current,
|
|
822
|
+
const operand = staticNumberCheckOperand(current, context)
|
|
805
823
|
if (operand != null) return staticAssertionAtomProblem(operand) ?? fallback
|
|
806
824
|
if (ts.isCallExpression(current)) return 'functionCall'
|
|
807
825
|
return 'directCheck'
|
|
808
826
|
}
|
|
809
827
|
|
|
810
|
-
function staticNumberCheckOperand(expression: ts.Expression,
|
|
828
|
+
function staticNumberCheckOperand(expression: ts.Expression, context: FunctionContext): ts.Expression | null {
|
|
811
829
|
if (!ts.isCallExpression(expression) || expression.questionDotToken != null
|
|
812
830
|
|| expression.arguments.length !== 1 || !ts.isPropertyAccessExpression(expression.expression)
|
|
813
831
|
|| expression.expression.questionDotToken != null) return null
|
|
814
832
|
const callee = expression.expression
|
|
815
|
-
return
|
|
833
|
+
return isStandardGlobal(callee.expression, 'Number', context)
|
|
816
834
|
&& (callee.name.text === 'isInteger' || callee.name.text === 'isFinite' || callee.name.text === 'isNaN')
|
|
817
835
|
? expression.arguments[0]!
|
|
818
836
|
: null
|
|
@@ -882,12 +900,10 @@ export function compoundAssignmentOperator(kind: ts.SyntaxKind): Extract<Instruc
|
|
|
882
900
|
}
|
|
883
901
|
}
|
|
884
902
|
|
|
885
|
-
// The shared value-producing branch shape:
|
|
886
|
-
//
|
|
887
|
-
//
|
|
888
|
-
//
|
|
889
|
-
// the logical operators are the two consumers; lowerIfStatement stays separate (no result
|
|
890
|
-
// value, arms may terminate, and assignments are allowed there).
|
|
903
|
+
// The shared value-producing branch shape: lower each arm in its own block, then join at
|
|
904
|
+
// a continuation whose single parameter carries the result. Arms are assignment-free, so
|
|
905
|
+
// the join needs no binding merge. One entry accepts an existing condition value; the other
|
|
906
|
+
// keeps a compound condition split into branches so each check can narrow later operands.
|
|
891
907
|
function lowerValueBranch(
|
|
892
908
|
node: ts.Expression,
|
|
893
909
|
condition: ValueID,
|
|
@@ -904,6 +920,44 @@ function lowerValueBranch(
|
|
|
904
920
|
whenFalse: {block: whenFalse, arguments: []},
|
|
905
921
|
site: addSite(context, node),
|
|
906
922
|
})
|
|
923
|
+
return joinValueBranches(
|
|
924
|
+
node,
|
|
925
|
+
whenTrue,
|
|
926
|
+
whenFalse,
|
|
927
|
+
lowerTrueArm,
|
|
928
|
+
lowerFalseArm,
|
|
929
|
+
context,
|
|
930
|
+
)
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
function lowerBranchingValue(
|
|
934
|
+
node: ts.Expression,
|
|
935
|
+
condition: ts.Expression,
|
|
936
|
+
lowerTrueArm: () => ValueID,
|
|
937
|
+
lowerFalseArm: () => ValueID,
|
|
938
|
+
context: FunctionContext,
|
|
939
|
+
): ValueID {
|
|
940
|
+
const whenTrue = createBlock(context)
|
|
941
|
+
const whenFalse = createBlock(context)
|
|
942
|
+
lowerBranchingCondition(condition, whenTrue, whenFalse, context)
|
|
943
|
+
return joinValueBranches(
|
|
944
|
+
node,
|
|
945
|
+
whenTrue,
|
|
946
|
+
whenFalse,
|
|
947
|
+
lowerTrueArm,
|
|
948
|
+
lowerFalseArm,
|
|
949
|
+
context,
|
|
950
|
+
)
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
function joinValueBranches(
|
|
954
|
+
node: ts.Expression,
|
|
955
|
+
whenTrue: number,
|
|
956
|
+
whenFalse: number,
|
|
957
|
+
lowerTrueArm: () => ValueID,
|
|
958
|
+
lowerFalseArm: () => ValueID,
|
|
959
|
+
context: FunctionContext,
|
|
960
|
+
): ValueID {
|
|
907
961
|
context.currentBlock = context.blocks[whenTrue]!
|
|
908
962
|
const trueValue = lowerTrueArm()
|
|
909
963
|
const trueBlock = context.currentBlock
|
|
@@ -937,28 +991,13 @@ function lowerConditionalExpression(expression: ts.ConditionalExpression, contex
|
|
|
937
991
|
// condition as one boolean expression (the previous shape) hid the conjuncts behind a
|
|
938
992
|
// joined block parameter no branch refinement could see through; a conversion pass on
|
|
939
993
|
// the owner's repo caught the asymmetry.
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
const falseValue = lowerExpression(expression.whenFalse, context)
|
|
948
|
-
const falseBlock = context.currentBlock
|
|
949
|
-
const continuation = createBlock(context, 1)
|
|
950
|
-
terminate(trueBlock, {
|
|
951
|
-
kind: 'jump',
|
|
952
|
-
target: {block: continuation, arguments: [trueValue]},
|
|
953
|
-
site: addSite(context, expression),
|
|
954
|
-
})
|
|
955
|
-
terminate(falseBlock, {
|
|
956
|
-
kind: 'jump',
|
|
957
|
-
target: {block: continuation, arguments: [falseValue]},
|
|
958
|
-
site: addSite(context, expression),
|
|
959
|
-
})
|
|
960
|
-
context.currentBlock = context.blocks[continuation]!
|
|
961
|
-
return context.currentBlock.parameters[0]!
|
|
994
|
+
return lowerBranchingValue(
|
|
995
|
+
expression,
|
|
996
|
+
expression.condition,
|
|
997
|
+
() => lowerExpression(expression.whenTrue, context),
|
|
998
|
+
() => lowerExpression(expression.whenFalse, context),
|
|
999
|
+
context,
|
|
1000
|
+
)
|
|
962
1001
|
}
|
|
963
1002
|
|
|
964
1003
|
// `a && b` evaluates b only when a is true and yields false otherwise; `a || b` mirrors it —
|
|
@@ -1017,10 +1056,9 @@ function lowerLogicalExpression(expression: ts.BinaryExpression, context: Functi
|
|
|
1017
1056
|
requireBooleanCondition(expression.left, context.checker)
|
|
1018
1057
|
requireBooleanCondition(expression.right, context.checker)
|
|
1019
1058
|
const isAnd = expression.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken
|
|
1020
|
-
|
|
1021
|
-
return lowerValueBranch(
|
|
1059
|
+
return lowerBranchingValue(
|
|
1022
1060
|
expression,
|
|
1023
|
-
|
|
1061
|
+
expression.left,
|
|
1024
1062
|
() => isAnd
|
|
1025
1063
|
? lowerExpression(expression.right, context)
|
|
1026
1064
|
: addInstruction(context, expression, {kind: 'booleanConstant', value: true}),
|
|
@@ -1122,7 +1160,7 @@ export function valueKind(type: ts.Type, checker: ts.TypeChecker, depth = 0): Va
|
|
|
1122
1160
|
}
|
|
1123
1161
|
|
|
1124
1162
|
function valueKindUncached(type: ts.Type, checker: ts.TypeChecker, depth: number): ValueKindResult {
|
|
1125
|
-
if ((type
|
|
1163
|
+
if (numberConstituent(type) != null) return 'number'
|
|
1126
1164
|
if ((type.flags & ts.TypeFlags.BooleanLike) !== 0) return 'boolean'
|
|
1127
1165
|
// Strings are carried without claims: a label or id must not reject the numeric
|
|
1128
1166
|
// contract of the function around it.
|
|
@@ -1412,22 +1450,6 @@ export function requireBooleanCondition(node: ts.Node, checker: ts.TypeChecker):
|
|
|
1412
1450
|
}
|
|
1413
1451
|
|
|
1414
1452
|
function requireAccessedPropertyKind(access: ts.PropertyAccessExpression, checker: ts.TypeChecker): void {
|
|
1415
|
-
// An optional property reads as its maybe-undefined value: declared kinds wrap it in
|
|
1416
|
-
// the undefined sentinel and object literals fill omitted ones explicitly, so a record
|
|
1417
|
-
// value always carries every property its static type declares — there is always
|
|
1418
|
-
// something honest to read.
|
|
1419
|
-
const receiverType = checker.getTypeAtLocation(access.expression)
|
|
1420
|
-
// For an optional read the receiver includes the missing sentinels; the property lives
|
|
1421
|
-
// on the non-missing part, which getNonNullableType strips to.
|
|
1422
|
-
const presentType = access.questionDotToken != null ? checker.getNonNullableType(receiverType) : receiverType
|
|
1423
|
-
const property = checker.getPropertyOfType(presentType, access.name.text)
|
|
1424
|
-
// point.toString type-checks on every object literal, but the record value carries only
|
|
1425
|
-
// its own properties — an inherited prototype member has no honest answer. The
|
|
1426
|
-
// ownership test is the .d.ts rule: a property symbol declared only in declaration
|
|
1427
|
-
// files was not written by the project, and on a project record that means prototype.
|
|
1428
|
-
if (valueKind(presentType, checker) === 'object' && property != null && declaredOnlyInDeclarationFiles(property)) {
|
|
1429
|
-
throw unsupported(access, {kind: 'prototypeMemberRead', property: access.name.text})
|
|
1430
|
-
}
|
|
1431
1453
|
const type = checker.getTypeAtLocation(access)
|
|
1432
1454
|
if (valueKind(type, checker) != null) return
|
|
1433
1455
|
throw unsupported(access, {kind: 'valueType', typeText: checker.typeToString(type)})
|
|
@@ -1438,14 +1460,12 @@ function resolvedSymbol(symbol: ts.Symbol | undefined, checker: ts.TypeChecker):
|
|
|
1438
1460
|
return (symbol.flags & ts.SymbolFlags.Alias) === 0 ? symbol : checker.getAliasedSymbol(symbol)
|
|
1439
1461
|
}
|
|
1440
1462
|
|
|
1441
|
-
function
|
|
1442
|
-
if (!ts.isIdentifier(expression) || expression.text !==
|
|
1443
|
-
return
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
if (!ts.isIdentifier(expression) || expression.text !== 'Number') return false
|
|
1448
|
-
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
|
+
)
|
|
1449
1469
|
}
|
|
1450
1470
|
|
|
1451
1471
|
function lowerElementAccess(access: ts.ElementAccessExpression, asserted: boolean, context: FunctionContext): ValueID {
|
|
@@ -1608,7 +1628,10 @@ function missingSentinelCheck(expression: ts.BinaryExpression, context: Function
|
|
|
1608
1628
|
const missing = ts.TypeFlags.Null | ts.TypeFlags.Undefined
|
|
1609
1629
|
const members = operandType.isUnion() ? operandType.types : [operandType]
|
|
1610
1630
|
const restMatches = members.every(member =>
|
|
1611
|
-
(member.flags & missing) !== 0
|
|
1631
|
+
(member.flags & missing) !== 0
|
|
1632
|
+
|| (typeofSide.flags === ts.TypeFlags.NumberLike
|
|
1633
|
+
? valueKind(member, context.checker) === 'number'
|
|
1634
|
+
: (member.flags & typeofSide.flags) !== 0))
|
|
1612
1635
|
&& members.some(member => (member.flags & missing) === 0)
|
|
1613
1636
|
const value = lowerExpression(typeofSide.operand.expression, context)
|
|
1614
1637
|
if (restMatches) {
|
|
@@ -1642,22 +1665,17 @@ function missingSentinelCheck(expression: ts.BinaryExpression, context: Function
|
|
|
1642
1665
|
})
|
|
1643
1666
|
}
|
|
1644
1667
|
|
|
1645
|
-
function isGlobalInfinity(expression: ts.Expression, checker: ts.TypeChecker): boolean {
|
|
1646
|
-
if (!ts.isIdentifier(expression) || expression.text !== 'Infinity') return false
|
|
1647
|
-
return declaredOnlyInDeclarationFiles(checker.getSymbolAtLocation(expression))
|
|
1648
|
-
}
|
|
1649
|
-
|
|
1650
1668
|
// Resolves an identifier read: a function-local binding first, then a module binding
|
|
1651
1669
|
// (which reads the binding's slot), then the global `Infinity` as an exact constant,
|
|
1652
1670
|
// else the identifier is unknown. A local or module binding named Infinity has a
|
|
1653
|
-
// different symbol and wins above;
|
|
1654
|
-
//
|
|
1671
|
+
// different symbol and wins above; only TypeScript's standard Infinity receives the
|
|
1672
|
+
// exact constant.
|
|
1655
1673
|
function identifierValue(symbol: ts.Symbol, node: ts.Identifier, context: FunctionContext): ValueID {
|
|
1656
1674
|
const local = context.bindings.get(symbol)
|
|
1657
1675
|
if (local != null) return local
|
|
1658
1676
|
const binding = context.moduleBindingsBySymbol.get(symbol)
|
|
1659
1677
|
if (binding != null) return addInstruction(context, node, {kind: 'moduleRead', binding})
|
|
1660
|
-
if (
|
|
1678
|
+
if (isStandardGlobal(node, 'Infinity', context)) {
|
|
1661
1679
|
return addInstruction(context, node, {kind: 'constant', value: Number.POSITIVE_INFINITY})
|
|
1662
1680
|
}
|
|
1663
1681
|
if (isUndefinedGlobal(node, context.checker)) {
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import * as ts from 'typescript'
|
|
2
|
+
|
|
3
|
+
export type TopLevelFunctionNode =
|
|
4
|
+
| ts.FunctionDeclaration
|
|
5
|
+
| ts.ArrowFunction
|
|
6
|
+
| ts.FunctionExpression
|
|
7
|
+
|
|
8
|
+
export type TopLevelFunctionUnit = {
|
|
9
|
+
name: ts.Identifier
|
|
10
|
+
declaration: TopLevelFunctionNode
|
|
11
|
+
initializer: ts.VariableDeclaration | null
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function callableSignature(
|
|
15
|
+
unit: TopLevelFunctionUnit,
|
|
16
|
+
checker: ts.TypeChecker,
|
|
17
|
+
): ts.Signature | null {
|
|
18
|
+
if (unit.initializer == null) {
|
|
19
|
+
return checker.getSignatureFromDeclaration(unit.declaration) ?? null
|
|
20
|
+
}
|
|
21
|
+
const signatures = checker.getSignaturesOfType(
|
|
22
|
+
checker.getTypeAtLocation(unit.name),
|
|
23
|
+
ts.SignatureKind.Call,
|
|
24
|
+
)
|
|
25
|
+
if (signatures.length !== 1) return null
|
|
26
|
+
const signature = signatures[0]!
|
|
27
|
+
const signatureDeclaration = signature.declaration
|
|
28
|
+
if (signatureDeclaration == null
|
|
29
|
+
|| signature.thisParameter != null
|
|
30
|
+
|| signature.parameters.length !== unit.declaration.parameters.length
|
|
31
|
+
|| signatureDeclaration.parameters.some(parameter =>
|
|
32
|
+
!ts.isParameter(parameter) || parameter.dotDotDotToken != null)) {
|
|
33
|
+
return null
|
|
34
|
+
}
|
|
35
|
+
return signature
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function topLevelFunctionUnits(sourceFile: ts.SourceFile): TopLevelFunctionUnit[] {
|
|
39
|
+
const units: TopLevelFunctionUnit[] = []
|
|
40
|
+
for (const statement of sourceFile.statements) {
|
|
41
|
+
if (ts.isFunctionDeclaration(statement) && statement.name != null) {
|
|
42
|
+
units.push({name: statement.name, declaration: statement, initializer: null})
|
|
43
|
+
continue
|
|
44
|
+
}
|
|
45
|
+
if (!ts.isVariableStatement(statement)
|
|
46
|
+
|| (statement.declarationList.flags & ts.NodeFlags.Const) === 0) continue
|
|
47
|
+
for (const initializer of statement.declarationList.declarations) {
|
|
48
|
+
if (!ts.isIdentifier(initializer.name) || initializer.initializer == null) continue
|
|
49
|
+
const declaration = directFunctionExpression(initializer.initializer)
|
|
50
|
+
if (declaration != null) {
|
|
51
|
+
units.push({name: initializer.name, declaration, initializer})
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return units
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function directFunctionExpression(
|
|
59
|
+
expression: ts.Expression,
|
|
60
|
+
): ts.ArrowFunction | ts.FunctionExpression | null {
|
|
61
|
+
return ts.isArrowFunction(expression) || ts.isFunctionExpression(expression)
|
|
62
|
+
? expression
|
|
63
|
+
: null
|
|
64
|
+
}
|
package/src/lower/module.ts
CHANGED
|
@@ -18,6 +18,8 @@ import {numericLiteralValue} from './literals.ts'
|
|
|
18
18
|
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
|
+
import {directFunctionExpression} from './function-unit.ts'
|
|
22
|
+
import {numberConstituent} from './numeric-intersection.ts'
|
|
21
23
|
import {lowerStatement} from './statements.ts'
|
|
22
24
|
|
|
23
25
|
export type ModuleScan = {
|
|
@@ -45,9 +47,16 @@ export function scanModuleBindings(sourceFile: ts.SourceFile, checker: ts.TypeCh
|
|
|
45
47
|
// the statement itself is skipped when the initializer reaches it, and functions
|
|
46
48
|
// reading the name are rejected as unknown identifiers.
|
|
47
49
|
if ((statement.declarationList.flags & (ts.NodeFlags.Let | ts.NodeFlags.Const)) === 0) continue
|
|
50
|
+
const isConst = (statement.declarationList.flags & ts.NodeFlags.Const) !== 0
|
|
48
51
|
for (const declarator of statement.declarationList.declarations) {
|
|
49
52
|
if (ts.isIdentifier(declarator.name)) {
|
|
50
|
-
register(
|
|
53
|
+
register(
|
|
54
|
+
declarator.name,
|
|
55
|
+
isConst && declarator.initializer != null
|
|
56
|
+
&& directFunctionExpression(declarator.initializer) != null
|
|
57
|
+
? {kind: 'function'}
|
|
58
|
+
: declaredCategory(declarator.name, checker),
|
|
59
|
+
)
|
|
51
60
|
continue
|
|
52
61
|
}
|
|
53
62
|
// `const {cols} = gridSize` at the top level: each destructured name is its own
|
|
@@ -189,11 +198,12 @@ function demoteModuleWritesInNode(
|
|
|
189
198
|
export function lowerModuleInitializer(
|
|
190
199
|
sourceFile: ts.SourceFile,
|
|
191
200
|
checker: ts.TypeChecker,
|
|
201
|
+
program: ts.Program,
|
|
192
202
|
functionsBySymbol: Map<ts.Symbol, TopLevelFunction>,
|
|
193
203
|
scan: ModuleScan,
|
|
194
204
|
sites: SourceSpan[],
|
|
195
205
|
): {initializer: FunctionIR; skips: InitializerSkip[]} {
|
|
196
|
-
const context = createFunctionContext(sourceFile, checker, functionsBySymbol, scan.bindingsBySymbol, sites)
|
|
206
|
+
const context = createFunctionContext(sourceFile, checker, program, functionsBySymbol, scan.bindingsBySymbol, sites)
|
|
197
207
|
const skips: InitializerSkip[] = []
|
|
198
208
|
const statements = sourceFile.statements
|
|
199
209
|
// Each statement gets its own catch: an unsupported one is skipped — its half-lowered
|
|
@@ -206,7 +216,7 @@ export function lowerModuleInitializer(
|
|
|
206
216
|
if (skippedAtTopLevel(statement)) continue
|
|
207
217
|
const recovery = snapshotLowering(context)
|
|
208
218
|
try {
|
|
209
|
-
assertAccepted(statement)
|
|
219
|
+
assertAccepted(statement, true)
|
|
210
220
|
if (ts.isVariableStatement(statement)) {
|
|
211
221
|
lowerTopLevelDeclarations(statement, context, scan)
|
|
212
222
|
continue
|
|
@@ -354,6 +364,12 @@ function lowerTopLevelDeclarations(statement: ts.VariableStatement, context: Fun
|
|
|
354
364
|
const symbol = context.checker.getSymbolAtLocation(declarator.name)
|
|
355
365
|
const binding = symbol == null ? undefined : scan.bindingsBySymbol.get(symbol)
|
|
356
366
|
if (binding == null) throw new LoweringStop(declarator, {kind: 'variableDeclarationShape'})
|
|
367
|
+
if ((statement.declarationList.flags & ts.NodeFlags.Const) !== 0
|
|
368
|
+
&& directFunctionExpression(declarator.initializer) != null) {
|
|
369
|
+
const marker = addInstruction(context, declarator.initializer, {kind: 'opaqueConstant'})
|
|
370
|
+
addInstruction(context, declarator, {kind: 'moduleWrite', binding, value: marker})
|
|
371
|
+
continue
|
|
372
|
+
}
|
|
357
373
|
const value = lowerExpression(declarator.initializer, context)
|
|
358
374
|
addInstruction(context, declarator, {kind: 'moduleWrite', binding, value})
|
|
359
375
|
}
|
|
@@ -361,8 +377,9 @@ function lowerTopLevelDeclarations(statement: ts.VariableStatement, context: Fun
|
|
|
361
377
|
|
|
362
378
|
function skippedAtTopLevel(statement: ts.Statement): boolean {
|
|
363
379
|
// `export {alreadyDeclaredName}` and import declarations create bindings but run nothing.
|
|
364
|
-
//
|
|
365
|
-
//
|
|
380
|
+
// Named declarations pass here: those become program.functions entries, so unsupported
|
|
381
|
+
// code inside them keeps the fully-analyzed publish gate honest. Const-bound functions
|
|
382
|
+
// remain variable statements and lower above as function-initialization markers. An
|
|
366
383
|
// anonymous `export default function` has no name to collect under, so it falls through
|
|
367
384
|
// to ordinary statement lowering, which records it as an initializer skip — otherwise
|
|
368
385
|
// its body would be runtime code invisible to every gate.
|
|
@@ -667,14 +684,15 @@ function declaredKindUncached(type: ts.Type, checker: ts.TypeChecker, seen: ts.T
|
|
|
667
684
|
|
|
668
685
|
function numericLiteralInterval(type: ts.Type): DeclaredNumberInterval | 'nonFinite' | null {
|
|
669
686
|
const members = type.isUnion() ? type.types : [type]
|
|
670
|
-
if (!members.every(member =>
|
|
671
|
-
(member.flags & ts.TypeFlags.NumberLiteral) !== 0
|
|
672
|
-
&& (member.flags & ts.TypeFlags.EnumLiteral) === 0)) return null
|
|
673
687
|
let lower = Infinity
|
|
674
688
|
let upper = -Infinity
|
|
675
689
|
let integer = true
|
|
676
690
|
for (const member of members) {
|
|
677
|
-
const
|
|
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
|
|
678
696
|
if (!Number.isFinite(value)) return 'nonFinite'
|
|
679
697
|
lower = Math.min(lower, value)
|
|
680
698
|
upper = Math.max(upper, value)
|
|
@@ -741,6 +759,7 @@ function demote(bindings: ModuleBindingIR[], binding: ModuleBindingID): void {
|
|
|
741
759
|
break
|
|
742
760
|
}
|
|
743
761
|
case 'kind':
|
|
762
|
+
case 'function':
|
|
744
763
|
case 'import':
|
|
745
764
|
case 'opaque':
|
|
746
765
|
break
|
|
@@ -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
|
+
}
|
package/src/lower/platform.ts
CHANGED
|
@@ -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,16 +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
|
-
//
|
|
47
|
-
//
|
|
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:
|
|
50
|
+
{path: ['Date', 'now'], call: true, fact: {lower: -maximumDateTime, upper: maximumDateTime, integer: true}},
|
|
51
|
+
// Math.random returns one of JavaScript's discrete floating-point numbers below 1.
|
|
52
|
+
// Writing the predecessor of 1 keeps the exact closed range without adding open bounds.
|
|
53
|
+
{path: ['Math', 'random'], call: true, fact: {lower: 0, upper: 0.9999999999999999, integer: false}},
|
|
50
54
|
]
|
|
51
55
|
|
|
52
|
-
// Matches a property chain rooted at a global
|
|
53
|
-
//
|
|
54
|
-
|
|
55
|
-
|
|
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 {
|
|
56
64
|
const parts: string[] = []
|
|
57
65
|
let current: ts.Expression = expression
|
|
58
66
|
while (ts.isPropertyAccessExpression(current)) {
|
|
@@ -66,14 +74,22 @@ export function platformFact(expression: ts.Expression, call: boolean, checker:
|
|
|
66
74
|
&& candidate.path.length === parts.length
|
|
67
75
|
&& candidate.path.every((segment, index) => segment === parts[index]))
|
|
68
76
|
if (entry == null) return null
|
|
69
|
-
if (!
|
|
77
|
+
if (!hasDefaultLibraryDeclaration(checker.getSymbolAtLocation(current), program)) return null
|
|
70
78
|
return entry.fact
|
|
71
79
|
}
|
|
72
80
|
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
|
|
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
|
+
|
|
77
93
|
export function declaredOnlyInDeclarationFiles(symbol: ts.Symbol | undefined): boolean {
|
|
78
94
|
const declarations = symbol?.declarations
|
|
79
95
|
if (declarations == null || declarations.length === 0) return false
|