@chenglou/freerange 0.0.1 → 0.0.3
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 +12 -0
- package/README.md +237 -67
- package/dist/fr.js +7821 -0
- package/fr.ts +0 -2
- package/package.json +6 -3
- package/src/audit.ts +2 -1
- 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 -0
- package/src/lower/accept.ts +6 -2
- package/src/lower/context.ts +11 -5
- package/src/lower/expression.ts +61 -33
- package/src/lower/function-unit.ts +64 -0
- package/src/lower/module.ts +20 -4
- package/src/lower/platform.ts +3 -0
- package/src/lower/program.ts +62 -34
- package/src/lower/statements.ts +3 -8
- package/src/lower/static-intrinsics.ts +9 -8
- package/src/project.ts +2 -2
- package/src/report/index.ts +2 -0
- package/src/requirements/infer.ts +76 -25
package/src/lower/expression.ts
CHANGED
|
@@ -420,7 +420,10 @@ export function lowerExpression(expression: ts.Expression, context: FunctionCont
|
|
|
420
420
|
arguments_.push(lowerParameterDefault(default_, parameter.initializer, context))
|
|
421
421
|
continue
|
|
422
422
|
}
|
|
423
|
-
|
|
423
|
+
const callableParameter = callee.signature?.declaration?.parameters[index]
|
|
424
|
+
if (callableParameter != null
|
|
425
|
+
&& ts.isParameter(callableParameter)
|
|
426
|
+
&& callableParameter.questionToken != null) {
|
|
424
427
|
arguments_.push(addInstruction(context, parameter, {kind: 'nullishConstant', sentinel: 'undefined'}))
|
|
425
428
|
continue
|
|
426
429
|
}
|
|
@@ -452,7 +455,12 @@ export function lowerExpression(expression: ts.Expression, context: FunctionCont
|
|
|
452
455
|
context,
|
|
453
456
|
))
|
|
454
457
|
}
|
|
455
|
-
return addInstruction(context, current, {
|
|
458
|
+
return addInstruction(context, current, {
|
|
459
|
+
kind: 'call',
|
|
460
|
+
function: callee.id,
|
|
461
|
+
arguments: arguments_,
|
|
462
|
+
binding: callee.binding,
|
|
463
|
+
})
|
|
456
464
|
}
|
|
457
465
|
if (ts.isPropertyAccessExpression(current.expression)) {
|
|
458
466
|
const platformCall = current.arguments.length === 0 ? platformFact(current.expression, true, context.checker) : null
|
|
@@ -882,12 +890,10 @@ export function compoundAssignmentOperator(kind: ts.SyntaxKind): Extract<Instruc
|
|
|
882
890
|
}
|
|
883
891
|
}
|
|
884
892
|
|
|
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).
|
|
893
|
+
// The shared value-producing branch shape: lower each arm in its own block, then join at
|
|
894
|
+
// a continuation whose single parameter carries the result. Arms are assignment-free, so
|
|
895
|
+
// the join needs no binding merge. One entry accepts an existing condition value; the other
|
|
896
|
+
// keeps a compound condition split into branches so each check can narrow later operands.
|
|
891
897
|
function lowerValueBranch(
|
|
892
898
|
node: ts.Expression,
|
|
893
899
|
condition: ValueID,
|
|
@@ -904,6 +910,44 @@ function lowerValueBranch(
|
|
|
904
910
|
whenFalse: {block: whenFalse, arguments: []},
|
|
905
911
|
site: addSite(context, node),
|
|
906
912
|
})
|
|
913
|
+
return joinValueBranches(
|
|
914
|
+
node,
|
|
915
|
+
whenTrue,
|
|
916
|
+
whenFalse,
|
|
917
|
+
lowerTrueArm,
|
|
918
|
+
lowerFalseArm,
|
|
919
|
+
context,
|
|
920
|
+
)
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
function lowerBranchingValue(
|
|
924
|
+
node: ts.Expression,
|
|
925
|
+
condition: ts.Expression,
|
|
926
|
+
lowerTrueArm: () => ValueID,
|
|
927
|
+
lowerFalseArm: () => ValueID,
|
|
928
|
+
context: FunctionContext,
|
|
929
|
+
): ValueID {
|
|
930
|
+
const whenTrue = createBlock(context)
|
|
931
|
+
const whenFalse = createBlock(context)
|
|
932
|
+
lowerBranchingCondition(condition, whenTrue, whenFalse, context)
|
|
933
|
+
return joinValueBranches(
|
|
934
|
+
node,
|
|
935
|
+
whenTrue,
|
|
936
|
+
whenFalse,
|
|
937
|
+
lowerTrueArm,
|
|
938
|
+
lowerFalseArm,
|
|
939
|
+
context,
|
|
940
|
+
)
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
function joinValueBranches(
|
|
944
|
+
node: ts.Expression,
|
|
945
|
+
whenTrue: number,
|
|
946
|
+
whenFalse: number,
|
|
947
|
+
lowerTrueArm: () => ValueID,
|
|
948
|
+
lowerFalseArm: () => ValueID,
|
|
949
|
+
context: FunctionContext,
|
|
950
|
+
): ValueID {
|
|
907
951
|
context.currentBlock = context.blocks[whenTrue]!
|
|
908
952
|
const trueValue = lowerTrueArm()
|
|
909
953
|
const trueBlock = context.currentBlock
|
|
@@ -937,28 +981,13 @@ function lowerConditionalExpression(expression: ts.ConditionalExpression, contex
|
|
|
937
981
|
// condition as one boolean expression (the previous shape) hid the conjuncts behind a
|
|
938
982
|
// joined block parameter no branch refinement could see through; a conversion pass on
|
|
939
983
|
// 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]!
|
|
984
|
+
return lowerBranchingValue(
|
|
985
|
+
expression,
|
|
986
|
+
expression.condition,
|
|
987
|
+
() => lowerExpression(expression.whenTrue, context),
|
|
988
|
+
() => lowerExpression(expression.whenFalse, context),
|
|
989
|
+
context,
|
|
990
|
+
)
|
|
962
991
|
}
|
|
963
992
|
|
|
964
993
|
// `a && b` evaluates b only when a is true and yields false otherwise; `a || b` mirrors it —
|
|
@@ -1017,10 +1046,9 @@ function lowerLogicalExpression(expression: ts.BinaryExpression, context: Functi
|
|
|
1017
1046
|
requireBooleanCondition(expression.left, context.checker)
|
|
1018
1047
|
requireBooleanCondition(expression.right, context.checker)
|
|
1019
1048
|
const isAnd = expression.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken
|
|
1020
|
-
|
|
1021
|
-
return lowerValueBranch(
|
|
1049
|
+
return lowerBranchingValue(
|
|
1022
1050
|
expression,
|
|
1023
|
-
|
|
1051
|
+
expression.left,
|
|
1024
1052
|
() => isAnd
|
|
1025
1053
|
? lowerExpression(expression.right, context)
|
|
1026
1054
|
: addInstruction(context, expression, {kind: 'booleanConstant', value: true}),
|
|
@@ -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,7 @@ 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'
|
|
21
22
|
import {lowerStatement} from './statements.ts'
|
|
22
23
|
|
|
23
24
|
export type ModuleScan = {
|
|
@@ -45,9 +46,16 @@ export function scanModuleBindings(sourceFile: ts.SourceFile, checker: ts.TypeCh
|
|
|
45
46
|
// the statement itself is skipped when the initializer reaches it, and functions
|
|
46
47
|
// reading the name are rejected as unknown identifiers.
|
|
47
48
|
if ((statement.declarationList.flags & (ts.NodeFlags.Let | ts.NodeFlags.Const)) === 0) continue
|
|
49
|
+
const isConst = (statement.declarationList.flags & ts.NodeFlags.Const) !== 0
|
|
48
50
|
for (const declarator of statement.declarationList.declarations) {
|
|
49
51
|
if (ts.isIdentifier(declarator.name)) {
|
|
50
|
-
register(
|
|
52
|
+
register(
|
|
53
|
+
declarator.name,
|
|
54
|
+
isConst && declarator.initializer != null
|
|
55
|
+
&& directFunctionExpression(declarator.initializer) != null
|
|
56
|
+
? {kind: 'function'}
|
|
57
|
+
: declaredCategory(declarator.name, checker),
|
|
58
|
+
)
|
|
51
59
|
continue
|
|
52
60
|
}
|
|
53
61
|
// `const {cols} = gridSize` at the top level: each destructured name is its own
|
|
@@ -206,7 +214,7 @@ export function lowerModuleInitializer(
|
|
|
206
214
|
if (skippedAtTopLevel(statement)) continue
|
|
207
215
|
const recovery = snapshotLowering(context)
|
|
208
216
|
try {
|
|
209
|
-
assertAccepted(statement)
|
|
217
|
+
assertAccepted(statement, true)
|
|
210
218
|
if (ts.isVariableStatement(statement)) {
|
|
211
219
|
lowerTopLevelDeclarations(statement, context, scan)
|
|
212
220
|
continue
|
|
@@ -354,6 +362,12 @@ function lowerTopLevelDeclarations(statement: ts.VariableStatement, context: Fun
|
|
|
354
362
|
const symbol = context.checker.getSymbolAtLocation(declarator.name)
|
|
355
363
|
const binding = symbol == null ? undefined : scan.bindingsBySymbol.get(symbol)
|
|
356
364
|
if (binding == null) throw new LoweringStop(declarator, {kind: 'variableDeclarationShape'})
|
|
365
|
+
if ((statement.declarationList.flags & ts.NodeFlags.Const) !== 0
|
|
366
|
+
&& directFunctionExpression(declarator.initializer) != null) {
|
|
367
|
+
const marker = addInstruction(context, declarator.initializer, {kind: 'opaqueConstant'})
|
|
368
|
+
addInstruction(context, declarator, {kind: 'moduleWrite', binding, value: marker})
|
|
369
|
+
continue
|
|
370
|
+
}
|
|
357
371
|
const value = lowerExpression(declarator.initializer, context)
|
|
358
372
|
addInstruction(context, declarator, {kind: 'moduleWrite', binding, value})
|
|
359
373
|
}
|
|
@@ -361,8 +375,9 @@ function lowerTopLevelDeclarations(statement: ts.VariableStatement, context: Fun
|
|
|
361
375
|
|
|
362
376
|
function skippedAtTopLevel(statement: ts.Statement): boolean {
|
|
363
377
|
// `export {alreadyDeclaredName}` and import declarations create bindings but run nothing.
|
|
364
|
-
//
|
|
365
|
-
//
|
|
378
|
+
// Named declarations pass here: those become program.functions entries, so unsupported
|
|
379
|
+
// code inside them keeps the fully-analyzed publish gate honest. Const-bound functions
|
|
380
|
+
// remain variable statements and lower above as function-initialization markers. An
|
|
366
381
|
// anonymous `export default function` has no name to collect under, so it falls through
|
|
367
382
|
// to ordinary statement lowering, which records it as an initializer skip — otherwise
|
|
368
383
|
// its body would be runtime code invisible to every gate.
|
|
@@ -741,6 +756,7 @@ function demote(bindings: ModuleBindingIR[], binding: ModuleBindingID): void {
|
|
|
741
756
|
break
|
|
742
757
|
}
|
|
743
758
|
case 'kind':
|
|
759
|
+
case 'function':
|
|
744
760
|
case 'import':
|
|
745
761
|
case 'opaque':
|
|
746
762
|
break
|
package/src/lower/platform.ts
CHANGED
|
@@ -47,6 +47,9 @@ const catalog: PlatformEntry[] = [
|
|
|
47
47
|
// resolution); Date.now is an integer count of milliseconds.
|
|
48
48
|
{path: ['performance', 'now'], call: true, fact: {lower: 0, upper: Number.MAX_VALUE, integer: false}},
|
|
49
49
|
{path: ['Date', 'now'], call: true, fact: {lower: 0, upper: Number.MAX_VALUE, integer: true}},
|
|
50
|
+
// Math.random returns one of JavaScript's discrete floating-point numbers below 1.
|
|
51
|
+
// Writing the predecessor of 1 keeps the exact closed range without adding open bounds.
|
|
52
|
+
{path: ['Math', 'random'], call: true, fact: {lower: 0, upper: 0.9999999999999999, integer: false}},
|
|
50
53
|
]
|
|
51
54
|
|
|
52
55
|
// Matches a property chain rooted at a global whose symbol resolves into a declaration
|
package/src/lower/program.ts
CHANGED
|
@@ -4,7 +4,8 @@ import {moduleInitializerName, nodeSpan, type DeclaredKind, type FunctionIR, typ
|
|
|
4
4
|
import type {CheckedSource} from '../typescript/check.ts'
|
|
5
5
|
import {assertAccepted, evalMention, typeCheckSuppressionMention} from './accept.ts'
|
|
6
6
|
import {addInstructionAtSite, addSite, createFunctionContext, LoweringStop, requiredSymbol, sealBlocks, terminate, unsupported, type MutableBlock, type TopLevelFunction} from './context.ts'
|
|
7
|
-
import {valueKind} from './expression.ts'
|
|
7
|
+
import {lowerExpression, valueKind} from './expression.ts'
|
|
8
|
+
import {callableSignature, topLevelFunctionUnits} from './function-unit.ts'
|
|
8
9
|
import {parameterDefaultFits, parameterDefaultLiteral, type ParameterDefaultLiteral} from './literals.ts'
|
|
9
10
|
import {declaredKind, lowerModuleInitializer, scanModuleBindings, tupleHasOptionalOrRestPositions, type ModuleScan} from './module.ts'
|
|
10
11
|
import {scanStaticAnnotations, type StaticAnnotation} from './static-intrinsics.ts'
|
|
@@ -12,10 +13,7 @@ import {lowerStatements} from './statements.ts'
|
|
|
12
13
|
|
|
13
14
|
export function lowerSource(checked: CheckedSource, baseDirectory: string = process.cwd()): ProgramIR {
|
|
14
15
|
const {sourceFile, checker} = checked
|
|
15
|
-
const declarations
|
|
16
|
-
for (const statement of sourceFile.statements) {
|
|
17
|
-
if (ts.isFunctionDeclaration(statement) && statement.name != null) declarations.push(statement)
|
|
18
|
-
}
|
|
16
|
+
const declarations = topLevelFunctionUnits(sourceFile)
|
|
19
17
|
const staticScan = scanStaticAnnotations(sourceFile, declarations, checker)
|
|
20
18
|
const recordStaticAnnotationIssues = (sites: SourceSpan[]): ProgramIR['staticAnnotationIssues'] =>
|
|
21
19
|
staticScan.outsideTopLevelFunctions.map(call => {
|
|
@@ -34,9 +32,9 @@ export function lowerSource(checked: CheckedSource, baseDirectory: string = proc
|
|
|
34
32
|
baseDirectory,
|
|
35
33
|
lineStarts: [...sourceFile.getLineStarts()],
|
|
36
34
|
sites,
|
|
37
|
-
functions: declarations.map((
|
|
35
|
+
functions: declarations.map((unit, index) => ({
|
|
38
36
|
kind: 'unsupported',
|
|
39
|
-
name:
|
|
37
|
+
name: unit.name.text,
|
|
40
38
|
hasStaticAnnotations: staticScan.functions[index]!.length > 0,
|
|
41
39
|
site: 0,
|
|
42
40
|
reason,
|
|
@@ -62,21 +60,33 @@ export function lowerSource(checked: CheckedSource, baseDirectory: string = proc
|
|
|
62
60
|
return rejectFile(nodeSpan(sourceFile, evalNode), {kind: 'evalInFile'})
|
|
63
61
|
}
|
|
64
62
|
const functionsBySymbol = new Map<ts.Symbol, TopLevelFunction>()
|
|
63
|
+
const scan = scanModuleBindings(sourceFile, checker)
|
|
64
|
+
const topLevelFunctions: TopLevelFunction[] = []
|
|
65
65
|
for (let index = 0; index < declarations.length; index++) {
|
|
66
|
-
const
|
|
66
|
+
const unit = declarations[index]!
|
|
67
67
|
// This loop runs outside the per-function catch below, so a missing symbol here is an
|
|
68
68
|
// invariant crash, not a recorded reason: a declaration name that type-checked always
|
|
69
69
|
// has a symbol.
|
|
70
|
-
const symbol = checker.getSymbolAtLocation(
|
|
71
|
-
if (symbol == null) throw new Error(`Function
|
|
72
|
-
|
|
70
|
+
const symbol = checker.getSymbolAtLocation(unit.name)
|
|
71
|
+
if (symbol == null) throw new Error(`Function ${unit.name.text} has no TypeScript symbol`)
|
|
72
|
+
const binding = scan.bindingsBySymbol.get(symbol)
|
|
73
|
+
if (unit.initializer != null && binding == null) {
|
|
74
|
+
throw new Error(`Const-bound function ${unit.name.text} has no module binding`)
|
|
75
|
+
}
|
|
76
|
+
const fn: TopLevelFunction = {
|
|
77
|
+
...unit,
|
|
78
|
+
id: index,
|
|
79
|
+
binding: unit.initializer == null ? null : binding!,
|
|
80
|
+
signature: callableSignature(unit, checker),
|
|
81
|
+
}
|
|
82
|
+
topLevelFunctions.push(fn)
|
|
83
|
+
functionsBySymbol.set(symbol, fn)
|
|
73
84
|
}
|
|
74
|
-
const scan = scanModuleBindings(sourceFile, checker)
|
|
75
85
|
const sites: SourceSpan[] = []
|
|
76
86
|
const staticAnnotationIssues = recordStaticAnnotationIssues(sites)
|
|
77
87
|
const functions: FunctionLowering[] = []
|
|
78
|
-
for (let index = 0; index <
|
|
79
|
-
const declaration =
|
|
88
|
+
for (let index = 0; index < topLevelFunctions.length; index++) {
|
|
89
|
+
const declaration = topLevelFunctions[index]!
|
|
80
90
|
const staticAnnotations = staticScan.functions[index]!
|
|
81
91
|
// A failed function lowering discards the half-built FunctionContext wholesale; only
|
|
82
92
|
// the name, annotation presence, offending node's site, and tagged reason survive.
|
|
@@ -87,7 +97,7 @@ export function lowerSource(checked: CheckedSource, baseDirectory: string = proc
|
|
|
87
97
|
sites.push(nodeSpan(sourceFile, error.node))
|
|
88
98
|
functions.push({
|
|
89
99
|
kind: 'unsupported',
|
|
90
|
-
name: declaration.name
|
|
100
|
+
name: declaration.name.text,
|
|
91
101
|
hasStaticAnnotations: staticAnnotations.length > 0,
|
|
92
102
|
site: sites.length - 1,
|
|
93
103
|
reason: error.reason,
|
|
@@ -109,7 +119,7 @@ export function lowerSource(checked: CheckedSource, baseDirectory: string = proc
|
|
|
109
119
|
}
|
|
110
120
|
|
|
111
121
|
function lowerFunction(
|
|
112
|
-
|
|
122
|
+
unit: TopLevelFunction,
|
|
113
123
|
staticAnnotations: StaticAnnotation[],
|
|
114
124
|
sourceFile: ts.SourceFile,
|
|
115
125
|
checker: ts.TypeChecker,
|
|
@@ -117,6 +127,7 @@ function lowerFunction(
|
|
|
117
127
|
scan: ModuleScan,
|
|
118
128
|
sites: SourceSpan[],
|
|
119
129
|
): FunctionIR {
|
|
130
|
+
const {declaration} = unit
|
|
120
131
|
for (const annotation of staticAnnotations) {
|
|
121
132
|
if (annotation.kind === 'invalid') {
|
|
122
133
|
throw unsupported(annotation.node, {kind: 'staticAssertionForm', problem: annotation.problem})
|
|
@@ -126,18 +137,24 @@ function lowerFunction(
|
|
|
126
137
|
// An async body returns a Promise and a generator returns an iterator; lowering either
|
|
127
138
|
// as if it ran synchronously would publish the body's values as the caller-visible
|
|
128
139
|
// result. Rejected wholesale.
|
|
129
|
-
if (declaration.asteriskToken != null
|
|
140
|
+
if ((!ts.isArrowFunction(declaration) && declaration.asteriskToken != null)
|
|
141
|
+
|| declaration.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword) === true) {
|
|
130
142
|
throw unsupported(declaration, {kind: 'asyncOrGeneratorFunction'})
|
|
131
143
|
}
|
|
132
144
|
assertAccepted(declaration)
|
|
133
|
-
const signature =
|
|
145
|
+
const signature = unit.signature
|
|
146
|
+
if (signature == null) {
|
|
147
|
+
throw unsupported(unit.name, {
|
|
148
|
+
kind: unit.initializer == null ? 'functionWithoutSignature' : 'constFunctionSignature',
|
|
149
|
+
})
|
|
150
|
+
}
|
|
134
151
|
// A type predicate (`shape is Circle`, `asserts x`) is the checker taking the author's
|
|
135
152
|
// word: callers' narrowing then exposes properties the analysis cannot confirm the
|
|
136
153
|
// value carries. Rejecting the declaring function stops every caller at the call.
|
|
137
|
-
if (
|
|
154
|
+
if (checker.getTypePredicateOfSignature(signature) != null) {
|
|
138
155
|
throw unsupported(declaration, {kind: 'typePredicate'})
|
|
139
156
|
}
|
|
140
|
-
const returnType =
|
|
157
|
+
const returnType = checker.getReturnTypeOfSignature(signature)
|
|
141
158
|
// `never` counts as returning nothing: the idiomatic annotation for an always-throwing
|
|
142
159
|
// helper (`function fail(code: number): never`), whose paths all end in throw — the
|
|
143
160
|
// always-throws analysis and the calleeAlwaysThrows caller stop handle the rest.
|
|
@@ -154,16 +171,21 @@ function lowerFunction(
|
|
|
154
171
|
scan.bindingsBySymbol,
|
|
155
172
|
sites,
|
|
156
173
|
staticAnnotations,
|
|
174
|
+
returnsVoid,
|
|
157
175
|
)
|
|
158
176
|
const entry = context.currentBlock
|
|
159
|
-
for (
|
|
177
|
+
for (let parameterIndex = 0; parameterIndex < declaration.parameters.length; parameterIndex++) {
|
|
178
|
+
const parameter = declaration.parameters[parameterIndex]!
|
|
179
|
+
const parameterType = unit.initializer == null
|
|
180
|
+
? checker.getTypeAtLocation(parameter)
|
|
181
|
+
: checker.getTypeOfSymbolAtLocation(signature.parameters[parameterIndex]!, signature.declaration!)
|
|
160
182
|
// `function area({width, height}: Size)` lowers as a synthetic record parameter plus
|
|
161
183
|
// one property read per name — the same classification named parameters use, the
|
|
162
184
|
// same reads body destructuring uses. The report metadata below keeps the local
|
|
163
185
|
// names, so a condition says `width` rather than `{width, height}.width`. Defaults
|
|
164
186
|
// and rest inside the pattern stay out, like the body form.
|
|
165
187
|
if (ts.isObjectBindingPattern(parameter.name)) {
|
|
166
|
-
const type = lowerParameterType(parameter, checker)
|
|
188
|
+
const type = lowerParameterType(parameter, parameterType, checker)
|
|
167
189
|
// The pattern text becomes the parameter's report name; a pattern the author wrapped
|
|
168
190
|
// across source lines would otherwise break the one-fact-per-line report format
|
|
169
191
|
// (`assumes: {` and orphan fragments — a corpus census caught eight of these).
|
|
@@ -201,7 +223,7 @@ function lowerFunction(
|
|
|
201
223
|
if (parameter.dotDotDotToken != null) {
|
|
202
224
|
throw unsupported(parameter, {kind: 'parameterType', typeText: `...${checker.typeToString(checker.getTypeAtLocation(parameter))}`, optionalOrRestTuple: false})
|
|
203
225
|
}
|
|
204
|
-
let type = lowerParameterType(parameter, checker)
|
|
226
|
+
let type = lowerParameterType(parameter, parameterType, checker)
|
|
205
227
|
// A default value applies whenever a caller omits the argument. Literal defaults can
|
|
206
228
|
// be represented exactly and checked against the declared assumptions: `zoom: number
|
|
207
229
|
// = 5` supplies a finite number. Anything else — `= Infinity`, `= readConfig()` —
|
|
@@ -218,7 +240,16 @@ function lowerFunction(
|
|
|
218
240
|
context.parameters.push({value, name: parameter.name.text, type, site: addSite(context, parameter), bindings: null})
|
|
219
241
|
}
|
|
220
242
|
lowerFiniteInputRequirements(context)
|
|
221
|
-
|
|
243
|
+
if (ts.isBlock(declaration.body)) {
|
|
244
|
+
lowerStatements(declaration.body.statements, context)
|
|
245
|
+
} else {
|
|
246
|
+
const value = lowerExpression(declaration.body, context)
|
|
247
|
+
terminate(context.currentBlock, {
|
|
248
|
+
kind: 'return',
|
|
249
|
+
value: returnsVoid ? null : value,
|
|
250
|
+
site: addSite(context, declaration.body),
|
|
251
|
+
})
|
|
252
|
+
}
|
|
222
253
|
if (context.currentBlock.terminator == null) {
|
|
223
254
|
if (!returnsVoid) {
|
|
224
255
|
// A non-void path reaching the end without a return is a per-path STOP, not a
|
|
@@ -234,12 +265,12 @@ function lowerFunction(
|
|
|
234
265
|
}
|
|
235
266
|
return {
|
|
236
267
|
kind: 'lowered',
|
|
237
|
-
name:
|
|
268
|
+
name: unit.name.text,
|
|
238
269
|
assertions: context.assertions,
|
|
239
270
|
parameters: context.parameters,
|
|
240
271
|
returnPropertyNames: declaredRecordReturnNames(returnType, checker),
|
|
241
272
|
entry: 0,
|
|
242
|
-
blocks: sealBlocks(context.blocks,
|
|
273
|
+
blocks: sealBlocks(context.blocks, unit.name.text),
|
|
243
274
|
}
|
|
244
275
|
}
|
|
245
276
|
|
|
@@ -287,11 +318,14 @@ function declaredRecordReturnNames(returnType: ts.Type, checker: ts.TypeChecker)
|
|
|
287
318
|
return null
|
|
288
319
|
}
|
|
289
320
|
|
|
290
|
-
function lowerParameterType(
|
|
321
|
+
function lowerParameterType(
|
|
322
|
+
parameter: ts.ParameterDeclaration,
|
|
323
|
+
type: ts.Type,
|
|
324
|
+
checker: ts.TypeChecker,
|
|
325
|
+
): DeclaredKind {
|
|
291
326
|
// The same recursive classification module bindings use: numbers, booleans, records
|
|
292
327
|
// (opaque leaves included — an id: string property is carried, not rejected), nullable
|
|
293
328
|
// wrappers, arrays, tuples, and bare opaque (a plain string parameter).
|
|
294
|
-
const type = checker.getTypeAtLocation(parameter)
|
|
295
329
|
const declared = declaredKind(type, checker, [])
|
|
296
330
|
if (declared == null) {
|
|
297
331
|
throw unsupported(parameter, {
|
|
@@ -302,9 +336,3 @@ function lowerParameterType(parameter: ts.ParameterDeclaration, checker: ts.Type
|
|
|
302
336
|
}
|
|
303
337
|
return declared
|
|
304
338
|
}
|
|
305
|
-
|
|
306
|
-
function functionReturnType(declaration: ts.FunctionDeclaration, checker: ts.TypeChecker): ts.Type {
|
|
307
|
-
const signature = checker.getSignatureFromDeclaration(declaration)
|
|
308
|
-
if (signature == null) throw unsupported(declaration, {kind: 'functionWithoutSignature'})
|
|
309
|
-
return checker.getReturnTypeOfSignature(signature)
|
|
310
|
-
}
|
package/src/lower/statements.ts
CHANGED
|
@@ -30,17 +30,12 @@ export function lowerStatement(statement: ts.Statement, context: FunctionContext
|
|
|
30
30
|
}
|
|
31
31
|
if (ts.isReturnStatement(statement)) {
|
|
32
32
|
let value = statement.expression == null ? null : lowerExpression(statement.expression, context)
|
|
33
|
+
if (context.returnsVoid) value = null
|
|
33
34
|
// A bare `return` in a function that returns a value IS `return undefined` — the
|
|
34
35
|
// common early exit in a `T | undefined` function. In void functions (and the module
|
|
35
36
|
// initializer) the return stays valueless.
|
|
36
|
-
if (value == null) {
|
|
37
|
-
|
|
38
|
-
const signature = enclosing == null ? undefined : context.checker.getSignatureFromDeclaration(enclosing)
|
|
39
|
-
const returnType = signature == null ? null : context.checker.getReturnTypeOfSignature(signature)
|
|
40
|
-
const returnsVoid = returnType == null || (returnType.flags & (ts.TypeFlags.Void | ts.TypeFlags.Undefined)) !== 0
|
|
41
|
-
if (!returnsVoid) {
|
|
42
|
-
value = addInstruction(context, statement, {kind: 'nullishConstant', sentinel: 'undefined'})
|
|
43
|
-
}
|
|
37
|
+
if (value == null && !context.returnsVoid) {
|
|
38
|
+
value = addInstruction(context, statement, {kind: 'nullishConstant', sentinel: 'undefined'})
|
|
44
39
|
}
|
|
45
40
|
terminate(context.currentBlock, {kind: 'return', value, site: addSite(context, statement)})
|
|
46
41
|
return
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as ts from 'typescript'
|
|
2
|
+
import type {TopLevelFunctionUnit} from './function-unit.ts'
|
|
2
3
|
|
|
3
4
|
type StaticAnnotationRole = 'requirement' | 'assertion'
|
|
4
5
|
|
|
@@ -19,19 +20,19 @@ type StaticAnnotationScan = {
|
|
|
19
20
|
|
|
20
21
|
export function scanStaticAnnotations(
|
|
21
22
|
sourceFile: ts.SourceFile,
|
|
22
|
-
|
|
23
|
+
functions: TopLevelFunctionUnit[],
|
|
23
24
|
checker: ts.TypeChecker,
|
|
24
25
|
): StaticAnnotationScan {
|
|
25
|
-
const ownerIndex = new Map<ts.
|
|
26
|
-
|
|
26
|
+
const ownerIndex = new Map<ts.SignatureDeclaration, number>(
|
|
27
|
+
functions.map((unit, index) => [unit.declaration, index]),
|
|
27
28
|
)
|
|
28
|
-
const callsByFunction =
|
|
29
|
+
const callsByFunction = functions.map(() => [] as ts.CallExpression[])
|
|
29
30
|
const outsideTopLevelFunctions: ts.CallExpression[] = []
|
|
30
31
|
|
|
31
32
|
const visit = (node: ts.Node): void => {
|
|
32
33
|
if (ts.isCallExpression(node) && isStaticIntent(node, checker)) {
|
|
33
34
|
const owner = ts.findAncestor(node, ts.isFunctionLike)
|
|
34
|
-
const index = owner
|
|
35
|
+
const index = owner == null ? undefined : ownerIndex.get(owner)
|
|
35
36
|
if (index == null) outsideTopLevelFunctions.push(node)
|
|
36
37
|
else callsByFunction[index]!.push(node)
|
|
37
38
|
}
|
|
@@ -40,12 +41,12 @@ export function scanStaticAnnotations(
|
|
|
40
41
|
visit(sourceFile)
|
|
41
42
|
|
|
42
43
|
return {
|
|
43
|
-
functions:
|
|
44
|
+
functions: functions.map((unit, index) => {
|
|
44
45
|
const calls = callsByFunction[index]!
|
|
45
46
|
const callSet = new Set(calls)
|
|
46
47
|
const leading = new Set<ts.CallExpression>()
|
|
47
|
-
if (declaration.body != null) {
|
|
48
|
-
for (const statement of declaration.body.statements) {
|
|
48
|
+
if (unit.declaration.body != null && ts.isBlock(unit.declaration.body)) {
|
|
49
|
+
for (const statement of unit.declaration.body.statements) {
|
|
49
50
|
if (!ts.isExpressionStatement(statement) || !ts.isCallExpression(statement.expression)
|
|
50
51
|
|| !callSet.has(statement.expression)) break
|
|
51
52
|
leading.add(statement.expression)
|
package/src/project.ts
CHANGED
|
@@ -301,7 +301,7 @@ function collectLintFindings({program, analysis}: DetailedAnalysis): LintFinding
|
|
|
301
301
|
addError(
|
|
302
302
|
issue.site,
|
|
303
303
|
'console-assert',
|
|
304
|
-
'console.assert is only supported inside a named top-level function
|
|
304
|
+
'console.assert is only supported inside a named top-level function',
|
|
305
305
|
)
|
|
306
306
|
}
|
|
307
307
|
for (const fn of analysis.functions) {
|
|
@@ -409,7 +409,7 @@ function formatLintPrefix(finding: LintFinding, rule: string, pretty: boolean):
|
|
|
409
409
|
}
|
|
410
410
|
|
|
411
411
|
function formatCoverage(coverage: ProjectCoverage): string {
|
|
412
|
-
return `coverage: ${coverage.analyzed}/${coverage.functions} named top-level
|
|
412
|
+
return `coverage: ${coverage.analyzed}/${coverage.functions} named top-level functions fully analyzed; ${coverage.partial} partially supported; ${coverage.unsupported} unsupported.`
|
|
413
413
|
}
|
|
414
414
|
|
|
415
415
|
// A target file analyzed on its own, with the output styling its project configures.
|
package/src/report/index.ts
CHANGED
|
@@ -837,6 +837,7 @@ function formatStop(stop: Stop, program: ProgramIR, analysis: ProgramAnalysis):
|
|
|
837
837
|
// read of one can only happen in the initializer's own top-level code.
|
|
838
838
|
case 'value':
|
|
839
839
|
case 'kind':
|
|
840
|
+
case 'function':
|
|
840
841
|
return `reads ${binding.name} before it is initialized (read at ${formatSite(program, stop.site)})`
|
|
841
842
|
}
|
|
842
843
|
}
|
|
@@ -905,6 +906,7 @@ export function formatUnsupportedReason(reason: UnsupportedReason): string {
|
|
|
905
906
|
case 'unknownIdentifier': return `unknown identifier ${reason.name}`
|
|
906
907
|
case 'missingSymbol': return 'node without a TypeScript symbol'
|
|
907
908
|
case 'functionWithoutSignature': return 'function without a TypeScript signature'
|
|
909
|
+
case 'constFunctionSignature': return 'a top-level const function whose declared call shape differs from its implementation'
|
|
908
910
|
case 'functionWithoutBody': return 'function declarations need bodies'
|
|
909
911
|
case 'destructuredParameter': return 'destructured parameters (take a named parameter and destructure it in the body)'
|
|
910
912
|
case 'parameterType': return reason.optionalOrRestTuple
|