@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/program.ts
CHANGED
|
@@ -4,18 +4,17 @@ 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'
|
|
11
12
|
import {lowerStatements} from './statements.ts'
|
|
12
13
|
|
|
13
14
|
export function lowerSource(checked: CheckedSource, baseDirectory: string = process.cwd()): ProgramIR {
|
|
14
|
-
const {sourceFile,
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
if (ts.isFunctionDeclaration(statement) && statement.name != null) declarations.push(statement)
|
|
18
|
-
}
|
|
15
|
+
const {sourceFile, program} = checked
|
|
16
|
+
const checker = program.getTypeChecker()
|
|
17
|
+
const declarations = topLevelFunctionUnits(sourceFile)
|
|
19
18
|
const staticScan = scanStaticAnnotations(sourceFile, declarations, checker)
|
|
20
19
|
const recordStaticAnnotationIssues = (sites: SourceSpan[]): ProgramIR['staticAnnotationIssues'] =>
|
|
21
20
|
staticScan.outsideTopLevelFunctions.map(call => {
|
|
@@ -34,9 +33,9 @@ export function lowerSource(checked: CheckedSource, baseDirectory: string = proc
|
|
|
34
33
|
baseDirectory,
|
|
35
34
|
lineStarts: [...sourceFile.getLineStarts()],
|
|
36
35
|
sites,
|
|
37
|
-
functions: declarations.map((
|
|
36
|
+
functions: declarations.map((unit, index) => ({
|
|
38
37
|
kind: 'unsupported',
|
|
39
|
-
name:
|
|
38
|
+
name: unit.name.text,
|
|
40
39
|
hasStaticAnnotations: staticScan.functions[index]!.length > 0,
|
|
41
40
|
site: 0,
|
|
42
41
|
reason,
|
|
@@ -62,39 +61,51 @@ export function lowerSource(checked: CheckedSource, baseDirectory: string = proc
|
|
|
62
61
|
return rejectFile(nodeSpan(sourceFile, evalNode), {kind: 'evalInFile'})
|
|
63
62
|
}
|
|
64
63
|
const functionsBySymbol = new Map<ts.Symbol, TopLevelFunction>()
|
|
64
|
+
const scan = scanModuleBindings(sourceFile, checker)
|
|
65
|
+
const topLevelFunctions: TopLevelFunction[] = []
|
|
65
66
|
for (let index = 0; index < declarations.length; index++) {
|
|
66
|
-
const
|
|
67
|
+
const unit = declarations[index]!
|
|
67
68
|
// This loop runs outside the per-function catch below, so a missing symbol here is an
|
|
68
69
|
// invariant crash, not a recorded reason: a declaration name that type-checked always
|
|
69
70
|
// has a symbol.
|
|
70
|
-
const symbol = checker.getSymbolAtLocation(
|
|
71
|
-
if (symbol == null) throw new Error(`Function
|
|
72
|
-
|
|
71
|
+
const symbol = checker.getSymbolAtLocation(unit.name)
|
|
72
|
+
if (symbol == null) throw new Error(`Function ${unit.name.text} has no TypeScript symbol`)
|
|
73
|
+
const binding = scan.bindingsBySymbol.get(symbol)
|
|
74
|
+
if (unit.initializer != null && binding == null) {
|
|
75
|
+
throw new Error(`Const-bound function ${unit.name.text} has no module binding`)
|
|
76
|
+
}
|
|
77
|
+
const fn: TopLevelFunction = {
|
|
78
|
+
...unit,
|
|
79
|
+
id: index,
|
|
80
|
+
binding: unit.initializer == null ? null : binding!,
|
|
81
|
+
signature: callableSignature(unit, checker),
|
|
82
|
+
}
|
|
83
|
+
topLevelFunctions.push(fn)
|
|
84
|
+
functionsBySymbol.set(symbol, fn)
|
|
73
85
|
}
|
|
74
|
-
const scan = scanModuleBindings(sourceFile, checker)
|
|
75
86
|
const sites: SourceSpan[] = []
|
|
76
87
|
const staticAnnotationIssues = recordStaticAnnotationIssues(sites)
|
|
77
88
|
const functions: FunctionLowering[] = []
|
|
78
|
-
for (let index = 0; index <
|
|
79
|
-
const declaration =
|
|
89
|
+
for (let index = 0; index < topLevelFunctions.length; index++) {
|
|
90
|
+
const declaration = topLevelFunctions[index]!
|
|
80
91
|
const staticAnnotations = staticScan.functions[index]!
|
|
81
92
|
// A failed function lowering discards the half-built FunctionContext wholesale; only
|
|
82
93
|
// the name, annotation presence, offending node's site, and tagged reason survive.
|
|
83
94
|
try {
|
|
84
|
-
functions.push(lowerFunction(declaration, staticAnnotations, sourceFile, checker, functionsBySymbol, scan, sites))
|
|
95
|
+
functions.push(lowerFunction(declaration, staticAnnotations, sourceFile, checker, program, functionsBySymbol, scan, sites))
|
|
85
96
|
} catch (error) {
|
|
86
97
|
if (!(error instanceof LoweringStop)) throw error
|
|
87
98
|
sites.push(nodeSpan(sourceFile, error.node))
|
|
88
99
|
functions.push({
|
|
89
100
|
kind: 'unsupported',
|
|
90
|
-
name: declaration.name
|
|
101
|
+
name: declaration.name.text,
|
|
91
102
|
hasStaticAnnotations: staticAnnotations.length > 0,
|
|
92
103
|
site: sites.length - 1,
|
|
93
104
|
reason: error.reason,
|
|
94
105
|
})
|
|
95
106
|
}
|
|
96
107
|
}
|
|
97
|
-
const {initializer, skips} = lowerModuleInitializer(sourceFile, checker, functionsBySymbol, scan, sites)
|
|
108
|
+
const {initializer, skips} = lowerModuleInitializer(sourceFile, checker, program, functionsBySymbol, scan, sites)
|
|
98
109
|
return {
|
|
99
110
|
file: sourceFile.fileName,
|
|
100
111
|
baseDirectory,
|
|
@@ -109,14 +120,16 @@ export function lowerSource(checked: CheckedSource, baseDirectory: string = proc
|
|
|
109
120
|
}
|
|
110
121
|
|
|
111
122
|
function lowerFunction(
|
|
112
|
-
|
|
123
|
+
unit: TopLevelFunction,
|
|
113
124
|
staticAnnotations: StaticAnnotation[],
|
|
114
125
|
sourceFile: ts.SourceFile,
|
|
115
126
|
checker: ts.TypeChecker,
|
|
127
|
+
program: ts.Program,
|
|
116
128
|
functionsBySymbol: Map<ts.Symbol, TopLevelFunction>,
|
|
117
129
|
scan: ModuleScan,
|
|
118
130
|
sites: SourceSpan[],
|
|
119
131
|
): FunctionIR {
|
|
132
|
+
const {declaration} = unit
|
|
120
133
|
for (const annotation of staticAnnotations) {
|
|
121
134
|
if (annotation.kind === 'invalid') {
|
|
122
135
|
throw unsupported(annotation.node, {kind: 'staticAssertionForm', problem: annotation.problem})
|
|
@@ -126,18 +139,18 @@ function lowerFunction(
|
|
|
126
139
|
// An async body returns a Promise and a generator returns an iterator; lowering either
|
|
127
140
|
// as if it ran synchronously would publish the body's values as the caller-visible
|
|
128
141
|
// result. Rejected wholesale.
|
|
129
|
-
if (declaration.asteriskToken != null
|
|
142
|
+
if ((!ts.isArrowFunction(declaration) && declaration.asteriskToken != null)
|
|
143
|
+
|| declaration.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword) === true) {
|
|
130
144
|
throw unsupported(declaration, {kind: 'asyncOrGeneratorFunction'})
|
|
131
145
|
}
|
|
132
146
|
assertAccepted(declaration)
|
|
133
|
-
const signature =
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
throw unsupported(declaration, {kind: 'typePredicate'})
|
|
147
|
+
const signature = unit.signature
|
|
148
|
+
if (signature == null) {
|
|
149
|
+
throw unsupported(unit.name, {
|
|
150
|
+
kind: unit.initializer == null ? 'functionWithoutSignature' : 'constFunctionSignature',
|
|
151
|
+
})
|
|
139
152
|
}
|
|
140
|
-
const returnType =
|
|
153
|
+
const returnType = checker.getReturnTypeOfSignature(signature)
|
|
141
154
|
// `never` counts as returning nothing: the idiomatic annotation for an always-throwing
|
|
142
155
|
// helper (`function fail(code: number): never`), whose paths all end in throw — the
|
|
143
156
|
// always-throws analysis and the calleeAlwaysThrows caller stop handle the rest.
|
|
@@ -150,20 +163,26 @@ function lowerFunction(
|
|
|
150
163
|
const context = createFunctionContext(
|
|
151
164
|
sourceFile,
|
|
152
165
|
checker,
|
|
166
|
+
program,
|
|
153
167
|
functionsBySymbol,
|
|
154
168
|
scan.bindingsBySymbol,
|
|
155
169
|
sites,
|
|
156
170
|
staticAnnotations,
|
|
171
|
+
returnsVoid,
|
|
157
172
|
)
|
|
158
173
|
const entry = context.currentBlock
|
|
159
|
-
for (
|
|
174
|
+
for (let parameterIndex = 0; parameterIndex < declaration.parameters.length; parameterIndex++) {
|
|
175
|
+
const parameter = declaration.parameters[parameterIndex]!
|
|
176
|
+
const parameterType = unit.initializer == null
|
|
177
|
+
? checker.getTypeAtLocation(parameter)
|
|
178
|
+
: checker.getTypeOfSymbolAtLocation(signature.parameters[parameterIndex]!, signature.declaration!)
|
|
160
179
|
// `function area({width, height}: Size)` lowers as a synthetic record parameter plus
|
|
161
180
|
// one property read per name — the same classification named parameters use, the
|
|
162
181
|
// same reads body destructuring uses. The report metadata below keeps the local
|
|
163
182
|
// names, so a condition says `width` rather than `{width, height}.width`. Defaults
|
|
164
183
|
// and rest inside the pattern stay out, like the body form.
|
|
165
184
|
if (ts.isObjectBindingPattern(parameter.name)) {
|
|
166
|
-
const type = lowerParameterType(parameter, checker)
|
|
185
|
+
const type = lowerParameterType(parameter, parameterType, checker)
|
|
167
186
|
// The pattern text becomes the parameter's report name; a pattern the author wrapped
|
|
168
187
|
// across source lines would otherwise break the one-fact-per-line report format
|
|
169
188
|
// (`assumes: {` and orphan fragments — a corpus census caught eight of these).
|
|
@@ -201,7 +220,7 @@ function lowerFunction(
|
|
|
201
220
|
if (parameter.dotDotDotToken != null) {
|
|
202
221
|
throw unsupported(parameter, {kind: 'parameterType', typeText: `...${checker.typeToString(checker.getTypeAtLocation(parameter))}`, optionalOrRestTuple: false})
|
|
203
222
|
}
|
|
204
|
-
let type = lowerParameterType(parameter, checker)
|
|
223
|
+
let type = lowerParameterType(parameter, parameterType, checker)
|
|
205
224
|
// A default value applies whenever a caller omits the argument. Literal defaults can
|
|
206
225
|
// be represented exactly and checked against the declared assumptions: `zoom: number
|
|
207
226
|
// = 5` supplies a finite number. Anything else — `= Infinity`, `= readConfig()` —
|
|
@@ -218,7 +237,16 @@ function lowerFunction(
|
|
|
218
237
|
context.parameters.push({value, name: parameter.name.text, type, site: addSite(context, parameter), bindings: null})
|
|
219
238
|
}
|
|
220
239
|
lowerFiniteInputRequirements(context)
|
|
221
|
-
|
|
240
|
+
if (ts.isBlock(declaration.body)) {
|
|
241
|
+
lowerStatements(declaration.body.statements, context)
|
|
242
|
+
} else {
|
|
243
|
+
const value = lowerExpression(declaration.body, context)
|
|
244
|
+
terminate(context.currentBlock, {
|
|
245
|
+
kind: 'return',
|
|
246
|
+
value: returnsVoid ? null : value,
|
|
247
|
+
site: addSite(context, declaration.body),
|
|
248
|
+
})
|
|
249
|
+
}
|
|
222
250
|
if (context.currentBlock.terminator == null) {
|
|
223
251
|
if (!returnsVoid) {
|
|
224
252
|
// A non-void path reaching the end without a return is a per-path STOP, not a
|
|
@@ -234,12 +262,12 @@ function lowerFunction(
|
|
|
234
262
|
}
|
|
235
263
|
return {
|
|
236
264
|
kind: 'lowered',
|
|
237
|
-
name:
|
|
265
|
+
name: unit.name.text,
|
|
238
266
|
assertions: context.assertions,
|
|
239
267
|
parameters: context.parameters,
|
|
240
268
|
returnPropertyNames: declaredRecordReturnNames(returnType, checker),
|
|
241
269
|
entry: 0,
|
|
242
|
-
blocks: sealBlocks(context.blocks,
|
|
270
|
+
blocks: sealBlocks(context.blocks, unit.name.text),
|
|
243
271
|
}
|
|
244
272
|
}
|
|
245
273
|
|
|
@@ -287,11 +315,14 @@ function declaredRecordReturnNames(returnType: ts.Type, checker: ts.TypeChecker)
|
|
|
287
315
|
return null
|
|
288
316
|
}
|
|
289
317
|
|
|
290
|
-
function lowerParameterType(
|
|
318
|
+
function lowerParameterType(
|
|
319
|
+
parameter: ts.ParameterDeclaration,
|
|
320
|
+
type: ts.Type,
|
|
321
|
+
checker: ts.TypeChecker,
|
|
322
|
+
): DeclaredKind {
|
|
291
323
|
// The same recursive classification module bindings use: numbers, booleans, records
|
|
292
324
|
// (opaque leaves included — an id: string property is carried, not rejected), nullable
|
|
293
325
|
// wrappers, arrays, tuples, and bare opaque (a plain string parameter).
|
|
294
|
-
const type = checker.getTypeAtLocation(parameter)
|
|
295
326
|
const declared = declaredKind(type, checker, [])
|
|
296
327
|
if (declared == null) {
|
|
297
328
|
throw unsupported(parameter, {
|
|
@@ -302,9 +333,3 @@ function lowerParameterType(parameter: ts.ParameterDeclaration, checker: ts.Type
|
|
|
302
333
|
}
|
|
303
334
|
return declared
|
|
304
335
|
}
|
|
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
|
@@ -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) {
|
|
@@ -30,17 +29,12 @@ export function lowerStatement(statement: ts.Statement, context: FunctionContext
|
|
|
30
29
|
}
|
|
31
30
|
if (ts.isReturnStatement(statement)) {
|
|
32
31
|
let value = statement.expression == null ? null : lowerExpression(statement.expression, context)
|
|
32
|
+
if (context.returnsVoid) value = null
|
|
33
33
|
// A bare `return` in a function that returns a value IS `return undefined` — the
|
|
34
34
|
// common early exit in a `T | undefined` function. In void functions (and the module
|
|
35
35
|
// 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
|
-
}
|
|
36
|
+
if (value == null && !context.returnsVoid) {
|
|
37
|
+
value = addInstruction(context, statement, {kind: 'nullishConstant', sentinel: 'undefined'})
|
|
44
38
|
}
|
|
45
39
|
terminate(context.currentBlock, {kind: 'return', value, site: addSite(context, statement)})
|
|
46
40
|
return
|
|
@@ -494,15 +488,8 @@ function lowerVariableDeclarationList(declarations: ts.VariableDeclarationList,
|
|
|
494
488
|
: null
|
|
495
489
|
if (property == null) throw unsupported(element, {kind: 'variableDeclarationShape'})
|
|
496
490
|
const elementType = context.checker.getTypeAtLocation(element.name)
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
// Optional properties destructure like any other read now that the filling
|
|
500
|
-
// invariant guarantees the record value carries them as maybe-undefined (the old
|
|
501
|
-
// gate here predated the optionals milestone). Prototype members stay out via
|
|
502
|
-
// the same ownership rule property reads use.
|
|
503
|
-
if (propertySymbol != null && declaredOnlyInDeclarationFiles(propertySymbol)) {
|
|
504
|
-
throw unsupported(element, {kind: 'prototypeMemberRead', property})
|
|
505
|
-
}
|
|
491
|
+
// Optional properties destructure like other reads because the record carries
|
|
492
|
+
// omitted properties as maybe-undefined values.
|
|
506
493
|
if (valueKind(elementType, context.checker) == null) {
|
|
507
494
|
throw unsupported(element, {kind: 'valueType', typeText: context.checker.typeToString(elementType)})
|
|
508
495
|
}
|
|
@@ -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.
|
|
@@ -461,7 +461,7 @@ function analyzeProjectSource(
|
|
|
461
461
|
): DetailedAnalysis {
|
|
462
462
|
return analyzeCheckedSource({
|
|
463
463
|
sourceFile: source.sourceFile,
|
|
464
|
-
|
|
464
|
+
program: source.project.program,
|
|
465
465
|
}, reportBaseDirectory)
|
|
466
466
|
}
|
|
467
467
|
|
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
|
|
@@ -916,10 +918,8 @@ export function formatUnsupportedReason(reason: UnsupportedReason): string {
|
|
|
916
918
|
case 'computedPropertyName': return 'computed object property name'
|
|
917
919
|
case 'objectSpread': return 'object spread (list every field explicitly, e.g. {gain: config.gain})'
|
|
918
920
|
case 'asyncOrGeneratorFunction': return 'an async or generator function (the runtime result is a Promise or iterator, not the body\'s return value)'
|
|
919
|
-
case 'typePredicate': return 'a type predicate (the checker takes the predicate on faith; return a plain boolean and check properties where they are read)'
|
|
920
921
|
case 'protoProperty': return 'a property named __proto__ (prototype-setting syntax at runtime, not a data property)'
|
|
921
922
|
case 'enumMemberRead': return 'an enum member read (replace the enum with plain module consts, e.g. const directionUp = 1)'
|
|
922
|
-
case 'prototypeMemberRead': return `read of the inherited prototype member ${reason.property} (records carry only their own data properties)`
|
|
923
923
|
case 'binaryOperator': return reason.operator === 'in'
|
|
924
924
|
? 'the `in` operator (use a distinct string or boolean tag when property presence distinguishes union variants)'
|
|
925
925
|
: `binary operator ${reason.operator} (supported: + - * / %, comparisons, and boolean && || !)`
|
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
import type {SiteID, ValueID} from '../ir/ids.ts'
|
|
2
2
|
import type {InstructionIR} from '../ir/instructions.ts'
|
|
3
3
|
import type {FunctionIR} from '../ir/program.ts'
|
|
4
|
+
import {
|
|
5
|
+
createValueIdentityOwner,
|
|
6
|
+
sameValueIdentity,
|
|
7
|
+
type ValueIdentity,
|
|
8
|
+
type ValueIdentityOwner,
|
|
9
|
+
} from '../domain/value-identity.ts'
|
|
4
10
|
import type {InferredPrecondition, NumericExpression} from './model.ts'
|
|
5
11
|
|
|
6
12
|
export type ExpressionContext = {
|
|
7
13
|
parameterExpressions: Array<NumericExpression | null>
|
|
8
|
-
// Calls pass the caller's
|
|
9
|
-
// in a callee refer to the same
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
|
|
14
|
+
// Calls pass the caller's identities directly, so duplicate arguments and facts created
|
|
15
|
+
// in a callee refer to the same stored value. Local identities use the evaluation's
|
|
16
|
+
// owner token, which distinguishes separate calls without encoding the call path.
|
|
17
|
+
parameterIdentities: ValueIdentity[]
|
|
18
|
+
identityOwner: ValueIdentityOwner
|
|
19
|
+
identityByValue: Array<ValueIdentity | undefined>
|
|
13
20
|
parameterIndexByValue: Array<number | undefined>
|
|
14
21
|
instructionByValue: Array<InstructionIR | undefined>
|
|
15
22
|
instructionCount: number
|
|
@@ -18,21 +25,27 @@ export type ExpressionContext = {
|
|
|
18
25
|
export function createExpressionContext(
|
|
19
26
|
fn: FunctionIR,
|
|
20
27
|
parameterExpressions: Array<NumericExpression | null>,
|
|
21
|
-
|
|
22
|
-
|
|
28
|
+
parameterIdentities?: ValueIdentity[],
|
|
29
|
+
identityOwner = createValueIdentityOwner(),
|
|
23
30
|
): ExpressionContext {
|
|
24
|
-
const identityKeys = parameterIdentityKeys ?? fn.parameters.map((_, index) => `p${index}`)
|
|
25
|
-
if (identityKeys.length !== fn.parameters.length) {
|
|
26
|
-
throw new Error(`Expected ${fn.parameters.length} parameter identity keys for ${fn.name}`)
|
|
27
|
-
}
|
|
28
31
|
const context: ExpressionContext = {
|
|
29
32
|
parameterExpressions,
|
|
30
|
-
|
|
31
|
-
|
|
33
|
+
parameterIdentities: [],
|
|
34
|
+
identityOwner,
|
|
35
|
+
identityByValue: [],
|
|
32
36
|
parameterIndexByValue: [],
|
|
33
37
|
instructionByValue: [],
|
|
34
38
|
instructionCount: 0,
|
|
35
39
|
}
|
|
40
|
+
context.parameterIdentities = parameterIdentities
|
|
41
|
+
?? fn.parameters.map(parameter => ({
|
|
42
|
+
kind: 'local',
|
|
43
|
+
owner: identityOwner,
|
|
44
|
+
value: parameter.value,
|
|
45
|
+
}))
|
|
46
|
+
if (context.parameterIdentities.length !== fn.parameters.length) {
|
|
47
|
+
throw new Error(`Expected ${fn.parameters.length} parameter identities for ${fn.name}`)
|
|
48
|
+
}
|
|
36
49
|
for (let index = 0; index < fn.parameters.length; index++) {
|
|
37
50
|
context.parameterIndexByValue[fn.parameters[index]!.value] = index
|
|
38
51
|
}
|
|
@@ -159,29 +172,67 @@ export function staticRequirement(
|
|
|
159
172
|
return null
|
|
160
173
|
}
|
|
161
174
|
|
|
162
|
-
// A stable
|
|
175
|
+
// A stable identity for the runtime value an IR value holds. Forward value facts and exact
|
|
163
176
|
// same-value operations share this rule instead of maintaining separate notions of
|
|
164
177
|
// identity. Property and array reads are stable under the accepted subset's immutability
|
|
165
178
|
// rules; module and platform reads stay value-keyed because they may change between reads.
|
|
166
|
-
export function
|
|
179
|
+
export function canonicalValueIdentity(
|
|
180
|
+
value: ValueID,
|
|
181
|
+
context: ExpressionContext,
|
|
182
|
+
): ValueIdentity {
|
|
183
|
+
const cached = context.identityByValue[value]
|
|
184
|
+
if (cached != null) return cached
|
|
167
185
|
const stored = resolveStoredValue(value, context)
|
|
168
|
-
if (stored !== value)
|
|
186
|
+
if (stored !== value) {
|
|
187
|
+
const identity = canonicalValueIdentity(stored, context)
|
|
188
|
+
context.identityByValue[value] = identity
|
|
189
|
+
return identity
|
|
190
|
+
}
|
|
169
191
|
const parameterIndex = context.parameterIndexByValue[value]
|
|
170
|
-
if (parameterIndex != null)
|
|
192
|
+
if (parameterIndex != null) {
|
|
193
|
+
const identity = context.parameterIdentities[parameterIndex]
|
|
194
|
+
if (identity == null) throw new Error(`Missing identity for parameter ${parameterIndex}`)
|
|
195
|
+
context.identityByValue[value] = identity
|
|
196
|
+
return identity
|
|
197
|
+
}
|
|
171
198
|
const producer = context.instructionByValue[value]
|
|
199
|
+
let identity: ValueIdentity
|
|
172
200
|
if (producer?.kind === 'property') {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
201
|
+
identity = {
|
|
202
|
+
kind: 'property',
|
|
203
|
+
object: canonicalValueIdentity(producer.object, context),
|
|
204
|
+
property: producer.property,
|
|
205
|
+
}
|
|
206
|
+
} else if (producer?.kind === 'arrayLength') {
|
|
207
|
+
identity = {
|
|
208
|
+
kind: 'property',
|
|
209
|
+
object: canonicalValueIdentity(producer.array, context),
|
|
210
|
+
property: 'length',
|
|
211
|
+
}
|
|
212
|
+
} else if (producer?.kind === 'stringLength') {
|
|
213
|
+
identity = {
|
|
214
|
+
kind: 'property',
|
|
215
|
+
object: canonicalValueIdentity(producer.value, context),
|
|
216
|
+
property: 'length',
|
|
217
|
+
}
|
|
218
|
+
} else if (producer?.kind === 'arrayIndex') {
|
|
219
|
+
identity = {
|
|
220
|
+
kind: 'arrayIndex',
|
|
221
|
+
array: canonicalValueIdentity(producer.array, context),
|
|
222
|
+
index: canonicalValueIdentity(producer.index, context),
|
|
223
|
+
}
|
|
224
|
+
} else {
|
|
225
|
+
identity = {kind: 'local', owner: context.identityOwner, value}
|
|
179
226
|
}
|
|
180
|
-
|
|
227
|
+
context.identityByValue[value] = identity
|
|
228
|
+
return identity
|
|
181
229
|
}
|
|
182
230
|
|
|
183
231
|
export function sameRuntimeValue(left: ValueID, right: ValueID, context: ExpressionContext): boolean {
|
|
184
|
-
return left === right ||
|
|
232
|
+
return left === right || sameValueIdentity(
|
|
233
|
+
canonicalValueIdentity(left, context),
|
|
234
|
+
canonicalValueIdentity(right, context),
|
|
235
|
+
)
|
|
185
236
|
}
|
|
186
237
|
|
|
187
238
|
export function addPrecondition(preconditions: InferredPrecondition[], candidate: InferredPrecondition): void {
|
package/src/typescript/check.ts
CHANGED
|
@@ -4,7 +4,7 @@ import {TypeScriptDiagnosticsError} from './diagnostics.ts'
|
|
|
4
4
|
|
|
5
5
|
export type CheckedSource = {
|
|
6
6
|
sourceFile: ts.SourceFile
|
|
7
|
-
|
|
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,
|
|
55
|
+
return {sourceFile, program}
|
|
56
56
|
}
|