@chenglou/freerange 0.0.1
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 +15 -0
- package/LICENSE +21 -0
- package/README.md +242 -0
- package/fr.ts +33 -0
- package/package.json +39 -0
- package/src/analyze.ts +16 -0
- package/src/audit.ts +468 -0
- package/src/domain/number.ts +444 -0
- package/src/domain/value.ts +445 -0
- package/src/engine/analyze.ts +745 -0
- package/src/engine/outcome.ts +137 -0
- package/src/engine/state.ts +155 -0
- package/src/engine/transfer.ts +1723 -0
- package/src/index.ts +23 -0
- package/src/ir/finite-inputs.ts +51 -0
- package/src/ir/function-usage.ts +50 -0
- package/src/ir/ids.ts +10 -0
- package/src/ir/instructions.ts +164 -0
- package/src/ir/program.ts +446 -0
- package/src/lower/accept.ts +119 -0
- package/src/lower/context.ts +250 -0
- package/src/lower/expression.ts +1742 -0
- package/src/lower/literals.ts +84 -0
- package/src/lower/module.ts +748 -0
- package/src/lower/platform.ts +81 -0
- package/src/lower/program.ts +310 -0
- package/src/lower/statements.ts +553 -0
- package/src/lower/static-intrinsics.ts +87 -0
- package/src/project.ts +500 -0
- package/src/report/format-requirement.ts +110 -0
- package/src/report/index.ts +1132 -0
- package/src/requirements/infer.ts +334 -0
- package/src/requirements/model.ts +77 -0
- package/src/typescript/check.ts +56 -0
- package/src/typescript/diagnostics.ts +69 -0
- package/src/typescript/project.ts +101 -0
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
import * as ts from 'typescript'
|
|
2
|
+
import type {BlockID, ValueID} from '../ir/ids.ts'
|
|
3
|
+
import {
|
|
4
|
+
addInstruction,
|
|
5
|
+
addSite,
|
|
6
|
+
bindingsVisibleAfterBranch,
|
|
7
|
+
createBlock,
|
|
8
|
+
mergeAtContinuation,
|
|
9
|
+
requiredBranchBinding,
|
|
10
|
+
requiredSymbol,
|
|
11
|
+
terminate,
|
|
12
|
+
unsupported,
|
|
13
|
+
type FunctionContext,
|
|
14
|
+
type MutableBlock,
|
|
15
|
+
} from './context.ts'
|
|
16
|
+
import {taggedUnionTagRead, identifierAssignment, lowerBranchingCondition, lowerExpression, lowerStatementExpression, requireBooleanCondition, valueKind} from './expression.ts'
|
|
17
|
+
import {declaredOnlyInDeclarationFiles} from './platform.ts'
|
|
18
|
+
|
|
19
|
+
export function lowerStatements(statements: readonly ts.Statement[], context: FunctionContext): void {
|
|
20
|
+
for (const statement of statements) {
|
|
21
|
+
if (context.currentBlock.terminator != null) throw unsupported(statement, {kind: 'statementAfterReturn'})
|
|
22
|
+
lowerStatement(statement, context)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function lowerStatement(statement: ts.Statement, context: FunctionContext): void {
|
|
27
|
+
if (ts.isVariableStatement(statement)) {
|
|
28
|
+
lowerVariableDeclarationList(statement.declarationList, context)
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
if (ts.isReturnStatement(statement)) {
|
|
32
|
+
let value = statement.expression == null ? null : lowerExpression(statement.expression, context)
|
|
33
|
+
// A bare `return` in a function that returns a value IS `return undefined` — the
|
|
34
|
+
// common early exit in a `T | undefined` function. In void functions (and the module
|
|
35
|
+
// initializer) the return stays valueless.
|
|
36
|
+
if (value == null) {
|
|
37
|
+
const enclosing = ts.findAncestor(statement, ts.isFunctionDeclaration)
|
|
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
|
+
}
|
|
44
|
+
}
|
|
45
|
+
terminate(context.currentBlock, {kind: 'return', value, site: addSite(context, statement)})
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
if (ts.isExpressionStatement(statement)) {
|
|
49
|
+
lowerStatementExpression(statement.expression, context)
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
if (ts.isIfStatement(statement)) {
|
|
53
|
+
lowerIfStatement(statement, context)
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
if (ts.isForOfStatement(statement)) {
|
|
57
|
+
lowerForOfStatement(statement, context)
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
if (ts.isForStatement(statement)) {
|
|
61
|
+
lowerForStatement(statement, context)
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
if (ts.isWhileStatement(statement)) {
|
|
65
|
+
lowerWhileStatement(statement, context)
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
if (ts.isContinueStatement(statement)) {
|
|
69
|
+
lowerContinueStatement(statement, context)
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
if (ts.isBlock(statement)) {
|
|
73
|
+
lowerStatements(statement.statements, context)
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
if (ts.isSwitchStatement(statement)) {
|
|
77
|
+
lowerSwitchStatement(statement, context)
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
// Guard clauses: `if (columns === 0) throw new Error('bad grid')`. The branch
|
|
81
|
+
// refinement then proves the fall-through nonzero, and the thrown path simply ends.
|
|
82
|
+
if (ts.isThrowStatement(statement)) {
|
|
83
|
+
terminate(context.currentBlock, {kind: 'thrown', site: addSite(context, statement)})
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
throw unsupported(statement, {kind: 'statementForm', syntax: ts.SyntaxKind[statement.kind]})
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function lowerIfStatement(statement: ts.IfStatement, context: FunctionContext): void {
|
|
90
|
+
const bindingsBeforeBranch = new Map(context.bindings)
|
|
91
|
+
const whenTrue = createBlock(context)
|
|
92
|
+
const whenFalse = createBlock(context)
|
|
93
|
+
lowerBranchingCondition(statement.expression, whenTrue, whenFalse, context)
|
|
94
|
+
|
|
95
|
+
const trueBranch = lowerBranch(statement.thenStatement, whenTrue, bindingsBeforeBranch, context)
|
|
96
|
+
const falseBranch = statement.elseStatement == null
|
|
97
|
+
? {block: context.blocks[whenFalse]!, bindings: new Map(bindingsBeforeBranch)}
|
|
98
|
+
: lowerBranch(statement.elseStatement, whenFalse, bindingsBeforeBranch, context)
|
|
99
|
+
const continuingBranches = [trueBranch, falseBranch].filter(branch => branch.block.terminator == null)
|
|
100
|
+
if (continuingBranches.length === 0) {
|
|
101
|
+
context.currentBlock = trueBranch.block
|
|
102
|
+
context.bindings = bindingsBeforeBranch
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
if (continuingBranches.length === 1) {
|
|
106
|
+
const continuing = continuingBranches[0]!
|
|
107
|
+
context.currentBlock = continuing.block
|
|
108
|
+
context.bindings = bindingsVisibleAfterBranch(bindingsBeforeBranch, continuing.bindings)
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
mergeAtContinuation([trueBranch, falseBranch], bindingsBeforeBranch, statement, context)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Switch without fallthrough (owner decision): every non-empty case body must end in a
|
|
116
|
+
// top-level break or a return, stacked empty labels share the next body, default comes
|
|
117
|
+
// last. Under that rule a switch is exactly an if/else chain on ===, so the lowering is
|
|
118
|
+
// pure reuse — number subjects get the comparison narrowing (case 4 knows the subject is
|
|
119
|
+
// 4), string subjects get the unknown-boolean dispatch with both branches analyzed, and
|
|
120
|
+
// bodies that break merge at the exit with the same block-parameter machinery as if/else.
|
|
121
|
+
function lowerSwitchStatement(statement: ts.SwitchStatement, context: FunctionContext): void {
|
|
122
|
+
const subjectType = context.checker.getTypeAtLocation(statement.expression)
|
|
123
|
+
const subjectKind = valueKind(subjectType, context.checker)
|
|
124
|
+
// switch (route.type) is tagged-union dispatch: the subject becomes the union value
|
|
125
|
+
// itself and every case emits a tag check, so each body knows its exact shape — the
|
|
126
|
+
// same narrowing the === spelling gets.
|
|
127
|
+
const tagUnionExpression = taggedUnionTagRead(statement.expression, context)
|
|
128
|
+
// A possibly-missing string subject (mode: string | undefined) dispatches like the ===
|
|
129
|
+
// spelling does: the missing value simply matches no case, so the unknown-boolean arm
|
|
130
|
+
// covers it. Every non-missing member must be a string for that to hold.
|
|
131
|
+
const missingFlags = ts.TypeFlags.Null | ts.TypeFlags.Undefined
|
|
132
|
+
const nullableOpaqueSubject = subjectKind === 'nullable' && subjectType.isUnion()
|
|
133
|
+
&& subjectType.types.every(member =>
|
|
134
|
+
(member.flags & missingFlags) !== 0 || valueKind(member, context.checker) === 'opaque')
|
|
135
|
+
if (tagUnionExpression == null && subjectKind !== 'number' && subjectKind !== 'opaque' && !nullableOpaqueSubject) {
|
|
136
|
+
throw unsupported(statement.expression, {kind: 'switchSubject', typeText: context.checker.typeToString(subjectType)})
|
|
137
|
+
}
|
|
138
|
+
const subject = lowerExpression(tagUnionExpression ?? statement.expression, context)
|
|
139
|
+
|
|
140
|
+
// Group stacked empty labels with the body they share; reject a default that is not the
|
|
141
|
+
// last clause (JS would test later cases before running it — supporting that order buys
|
|
142
|
+
// nothing over writing default last).
|
|
143
|
+
type CaseGroup = {labels: ts.Expression[]; statements: ts.Statement[]; clause: ts.CaseOrDefaultClause}
|
|
144
|
+
const groups: CaseGroup[] = []
|
|
145
|
+
let pendingLabels: ts.Expression[] = []
|
|
146
|
+
let defaultGroup: CaseGroup | null = null
|
|
147
|
+
const clauses = statement.caseBlock.clauses
|
|
148
|
+
for (let index = 0; index < clauses.length; index++) {
|
|
149
|
+
const clause = clauses[index]!
|
|
150
|
+
if (ts.isDefaultClause(clause)) {
|
|
151
|
+
if (index !== clauses.length - 1 || pendingLabels.length > 0) {
|
|
152
|
+
throw unsupported(clause, {kind: 'switchDefaultNotLast'})
|
|
153
|
+
}
|
|
154
|
+
defaultGroup = {labels: [], statements: [...clause.statements], clause}
|
|
155
|
+
continue
|
|
156
|
+
}
|
|
157
|
+
pendingLabels.push(clause.expression)
|
|
158
|
+
if (clause.statements.length > 0) {
|
|
159
|
+
groups.push({labels: pendingLabels, statements: [...clause.statements], clause})
|
|
160
|
+
pendingLabels = []
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// Trailing empty labels with no body to share fall out of the switch at runtime; the
|
|
164
|
+
// no-fallthrough rule wants that written as an explicit body, so they reject too.
|
|
165
|
+
if (pendingLabels.length > 0) {
|
|
166
|
+
throw unsupported(statement, {kind: 'switchFallthrough'})
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const bindingsBefore = new Map(context.bindings)
|
|
170
|
+
// Bodies that ended in a break continue after the switch, as does the no-match path
|
|
171
|
+
// when there is no default; all of them merge at the continuation.
|
|
172
|
+
const exits: Array<{block: MutableBlock; bindings: Map<ts.Symbol, ValueID>}> = []
|
|
173
|
+
|
|
174
|
+
const lowerBody = (group: CaseGroup): void => {
|
|
175
|
+
const body = group.statements
|
|
176
|
+
const last = body[body.length - 1]
|
|
177
|
+
const endsWithBreak = last != null && ts.isBreakStatement(last)
|
|
178
|
+
lowerStatements(endsWithBreak ? body.slice(0, -1) : body, context)
|
|
179
|
+
if (context.currentBlock.terminator == null) {
|
|
180
|
+
if (!endsWithBreak) throw unsupported(group.clause, {kind: 'switchFallthrough'})
|
|
181
|
+
exits.push({block: context.currentBlock, bindings: context.bindings})
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
for (const group of groups) {
|
|
186
|
+
const bodyBlock = createBlock(context)
|
|
187
|
+
// Chain of label tests: each label's false edge goes to the next label, the last
|
|
188
|
+
// label's false edge to the next group (or the default / the no-match exit).
|
|
189
|
+
for (const label of group.labels) {
|
|
190
|
+
const labelKind = valueKind(context.checker.getTypeAtLocation(label), context.checker)
|
|
191
|
+
const effectiveSubjectKind = nullableOpaqueSubject ? 'opaque' : subjectKind
|
|
192
|
+
if (tagUnionExpression == null && labelKind !== effectiveSubjectKind) {
|
|
193
|
+
throw unsupported(label, {kind: 'switchLabel', typeText: context.checker.typeToString(context.checker.getTypeAtLocation(label))})
|
|
194
|
+
}
|
|
195
|
+
let condition: ValueID
|
|
196
|
+
if (tagUnionExpression != null) {
|
|
197
|
+
const unwrappedLabel = label
|
|
198
|
+
if (!ts.isStringLiteral(unwrappedLabel) && !ts.isNoSubstitutionTemplateLiteral(unwrappedLabel)) {
|
|
199
|
+
throw unsupported(label, {kind: 'switchLabel', typeText: context.checker.typeToString(context.checker.getTypeAtLocation(label))})
|
|
200
|
+
}
|
|
201
|
+
condition = addInstruction(context, label, {kind: 'tagCheck', union: subject, tagValue: unwrappedLabel.text, negated: false})
|
|
202
|
+
} else {
|
|
203
|
+
const labelValue = lowerExpression(label, context)
|
|
204
|
+
condition = effectiveSubjectKind === 'number'
|
|
205
|
+
? addInstruction(context, label, {kind: 'compare', operator: 'equal', left: subject, right: labelValue})
|
|
206
|
+
: addInstruction(context, label, {kind: 'unknownBoolean'})
|
|
207
|
+
}
|
|
208
|
+
const nextTest = createBlock(context)
|
|
209
|
+
terminate(context.currentBlock, {
|
|
210
|
+
kind: 'branch',
|
|
211
|
+
condition,
|
|
212
|
+
whenTrue: {block: bodyBlock, arguments: []},
|
|
213
|
+
whenFalse: {block: nextTest, arguments: []},
|
|
214
|
+
site: addSite(context, label),
|
|
215
|
+
})
|
|
216
|
+
context.currentBlock = context.blocks[nextTest]!
|
|
217
|
+
}
|
|
218
|
+
const afterTests = context.currentBlock
|
|
219
|
+
const bindingsAtTests = new Map(context.bindings)
|
|
220
|
+
context.currentBlock = context.blocks[bodyBlock]!
|
|
221
|
+
context.bindings = new Map(bindingsBefore)
|
|
222
|
+
lowerBody(group)
|
|
223
|
+
context.currentBlock = afterTests
|
|
224
|
+
context.bindings = bindingsAtTests
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (defaultGroup == null) {
|
|
228
|
+
exits.push({block: context.currentBlock, bindings: context.bindings})
|
|
229
|
+
} else {
|
|
230
|
+
lowerBody(defaultGroup)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (exits.length === 0) {
|
|
234
|
+
// Every path returned; subsequent statements land in a terminated block, where the
|
|
235
|
+
// statement-after-return rejection already speaks.
|
|
236
|
+
return
|
|
237
|
+
}
|
|
238
|
+
if (exits.length === 1) {
|
|
239
|
+
context.currentBlock = exits[0]!.block
|
|
240
|
+
context.bindings = bindingsVisibleAfterBranch(bindingsBefore, exits[0]!.bindings)
|
|
241
|
+
return
|
|
242
|
+
}
|
|
243
|
+
mergeAtContinuation(exits, bindingsBefore, statement, context)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// `for (const x of arr)` desugars to a counter loop: a synthetic counter rides the loop
|
|
247
|
+
// header as an extra block parameter (block parameters are plain ValueIDs, no symbol
|
|
248
|
+
// needed), the header compares it against the array's length, and the body's element read
|
|
249
|
+
// is in bounds by construction — the guard, the 0 start, and the +1 step are all
|
|
250
|
+
// synthetic values nothing can reassign. An empty array prunes the body entirely.
|
|
251
|
+
function lowerForOfStatement(statement: ts.ForOfStatement, context: FunctionContext): void {
|
|
252
|
+
if (!ts.isVariableDeclarationList(statement.initializer)
|
|
253
|
+
|| statement.initializer.declarations.length !== 1
|
|
254
|
+
|| !ts.isIdentifier(statement.initializer.declarations[0]!.name)) {
|
|
255
|
+
throw unsupported(statement.initializer, {kind: 'variableDeclarationShape'})
|
|
256
|
+
}
|
|
257
|
+
const elementName = statement.initializer.declarations[0]!.name
|
|
258
|
+
const elementType = context.checker.getTypeAtLocation(elementName)
|
|
259
|
+
if (valueKind(elementType, context.checker) == null) {
|
|
260
|
+
throw unsupported(elementName, {kind: 'valueType', typeText: context.checker.typeToString(elementType)})
|
|
261
|
+
}
|
|
262
|
+
const arrayType = context.checker.getTypeAtLocation(statement.expression)
|
|
263
|
+
const arrayKind = valueKind(arrayType, context.checker)
|
|
264
|
+
if (arrayKind !== 'array' && arrayKind !== 'tuple') {
|
|
265
|
+
throw unsupported(statement.expression, {kind: 'valueType', typeText: context.checker.typeToString(arrayType)})
|
|
266
|
+
}
|
|
267
|
+
const array = lowerExpression(statement.expression, context)
|
|
268
|
+
const zero = addInstruction(context, statement, {kind: 'constant', value: 0})
|
|
269
|
+
|
|
270
|
+
const bindingsBeforeLoop = new Map(context.bindings)
|
|
271
|
+
const assigned = assignedSymbols([statement.statement], context.checker)
|
|
272
|
+
const carried = [...bindingsBeforeLoop.keys()].filter(symbol => assigned.has(symbol))
|
|
273
|
+
const header = createBlock(context, carried.length + 1, addSite(context, statement))
|
|
274
|
+
terminate(context.currentBlock, {
|
|
275
|
+
kind: 'jump',
|
|
276
|
+
target: {
|
|
277
|
+
block: header,
|
|
278
|
+
arguments: [...carried.map(symbol => requiredBranchBinding(symbol, bindingsBeforeLoop)), zero],
|
|
279
|
+
},
|
|
280
|
+
site: addSite(context, statement),
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
context.currentBlock = context.blocks[header]!
|
|
284
|
+
context.bindings = new Map(bindingsBeforeLoop)
|
|
285
|
+
for (let index = 0; index < carried.length; index++) {
|
|
286
|
+
context.bindings.set(carried[index]!, context.currentBlock.parameters[index]!)
|
|
287
|
+
}
|
|
288
|
+
const counter = context.currentBlock.parameters[carried.length]!
|
|
289
|
+
const length = addInstruction(context, statement, {kind: 'arrayLength', array})
|
|
290
|
+
const condition = addInstruction(context, statement, {kind: 'compare', operator: 'lessThan', left: counter, right: length})
|
|
291
|
+
const conditionBindings = new Map(context.bindings)
|
|
292
|
+
const body = createBlock(context)
|
|
293
|
+
const exit = createBlock(context)
|
|
294
|
+
terminate(context.currentBlock, {
|
|
295
|
+
kind: 'branch',
|
|
296
|
+
condition,
|
|
297
|
+
whenTrue: {block: body, arguments: []},
|
|
298
|
+
whenFalse: {block: exit, arguments: []},
|
|
299
|
+
site: addSite(context, statement),
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
// The counter is a raw header parameter, not a symbol binding, so a continue cannot
|
|
303
|
+
// observe a stale value: the bump is emitted fresh at each back edge (the body end here,
|
|
304
|
+
// and every continue site).
|
|
305
|
+
const advance = (advanceContext: FunctionContext): ValueID[] => {
|
|
306
|
+
const one = addInstruction(advanceContext, statement, {kind: 'constant', value: 1})
|
|
307
|
+
const next = addInstruction(advanceContext, statement, {kind: 'binary', operator: 'add', left: counter, right: one})
|
|
308
|
+
return [next]
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
context.currentBlock = context.blocks[body]!
|
|
312
|
+
context.bindings = new Map(conditionBindings)
|
|
313
|
+
context.bindings.set(
|
|
314
|
+
requiredSymbol(elementName, context.checker),
|
|
315
|
+
addInstruction(context, statement, {
|
|
316
|
+
kind: 'arrayIndex',
|
|
317
|
+
array,
|
|
318
|
+
index: counter,
|
|
319
|
+
mode: 'bare',
|
|
320
|
+
}),
|
|
321
|
+
)
|
|
322
|
+
context.loops.push({header, carried, advance})
|
|
323
|
+
lowerStatement(statement.statement, context)
|
|
324
|
+
context.loops.pop()
|
|
325
|
+
if (context.currentBlock.terminator == null) {
|
|
326
|
+
const extra = advance(context)
|
|
327
|
+
terminate(context.currentBlock, {
|
|
328
|
+
kind: 'jump',
|
|
329
|
+
target: {
|
|
330
|
+
block: header,
|
|
331
|
+
arguments: [...carried.map(symbol => requiredBranchBinding(symbol, context.bindings)), ...extra],
|
|
332
|
+
},
|
|
333
|
+
site: addSite(context, statement),
|
|
334
|
+
})
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
context.currentBlock = context.blocks[exit]!
|
|
338
|
+
context.bindings = new Map(conditionBindings)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function lowerForStatement(statement: ts.ForStatement, context: FunctionContext): void {
|
|
342
|
+
if (statement.initializer != null) {
|
|
343
|
+
if (ts.isVariableDeclarationList(statement.initializer)) {
|
|
344
|
+
lowerVariableDeclarationList(statement.initializer, context)
|
|
345
|
+
} else {
|
|
346
|
+
lowerStatementExpression(statement.initializer, context)
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
if (statement.condition == null) throw unsupported(statement, {kind: 'forLoopWithoutCondition'})
|
|
350
|
+
requireBooleanCondition(statement.condition, context.checker)
|
|
351
|
+
|
|
352
|
+
const bindingsBeforeLoop = new Map(context.bindings)
|
|
353
|
+
const scanned = statement.incrementor == null
|
|
354
|
+
? [statement.condition, statement.statement]
|
|
355
|
+
: [statement.condition, statement.statement, statement.incrementor]
|
|
356
|
+
const assigned = assignedSymbols(scanned, context.checker)
|
|
357
|
+
const carried = [...bindingsBeforeLoop.keys()].filter(symbol => assigned.has(symbol))
|
|
358
|
+
const header = createBlock(context, carried.length, addSite(context, statement))
|
|
359
|
+
terminate(context.currentBlock, {
|
|
360
|
+
kind: 'jump',
|
|
361
|
+
target: {block: header, arguments: carried.map(symbol => requiredBranchBinding(symbol, bindingsBeforeLoop))},
|
|
362
|
+
site: addSite(context, statement),
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
context.currentBlock = context.blocks[header]!
|
|
366
|
+
context.bindings = new Map(bindingsBeforeLoop)
|
|
367
|
+
for (let index = 0; index < carried.length; index++) {
|
|
368
|
+
context.bindings.set(carried[index]!, context.currentBlock.parameters[index]!)
|
|
369
|
+
}
|
|
370
|
+
const conditionBindings = new Map(context.bindings)
|
|
371
|
+
const body = createBlock(context)
|
|
372
|
+
const exit = createBlock(context)
|
|
373
|
+
lowerBranchingCondition(statement.condition, body, exit, context)
|
|
374
|
+
|
|
375
|
+
// A continue runs the incrementor before jumping back, same as the normal body end —
|
|
376
|
+
// JavaScript's order (continue in a for loop still advances the counter). An absent
|
|
377
|
+
// incrementor makes the loop while-shaped; progress then lives in the body.
|
|
378
|
+
const advance = (advanceContext: FunctionContext): ValueID[] => {
|
|
379
|
+
if (statement.incrementor != null) lowerStatementExpression(statement.incrementor, advanceContext)
|
|
380
|
+
return []
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
context.currentBlock = context.blocks[body]!
|
|
384
|
+
context.bindings = new Map(conditionBindings)
|
|
385
|
+
context.loops.push({header, carried, advance})
|
|
386
|
+
lowerStatement(statement.statement, context)
|
|
387
|
+
context.loops.pop()
|
|
388
|
+
if (context.currentBlock.terminator == null) {
|
|
389
|
+
advance(context)
|
|
390
|
+
terminate(context.currentBlock, {
|
|
391
|
+
kind: 'jump',
|
|
392
|
+
target: {block: header, arguments: carried.map(symbol => requiredBranchBinding(symbol, context.bindings))},
|
|
393
|
+
site: addSite(context, statement),
|
|
394
|
+
})
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
context.currentBlock = context.blocks[exit]!
|
|
398
|
+
context.bindings = conditionBindings
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// `while (cond) body` is a for loop with no initializer and no incrementor: the same
|
|
402
|
+
// header/body/exit blocks, the same carried-binding block parameters, the same widening
|
|
403
|
+
// and fixed point at the header. Progress lives in the body's own assignments, so a body
|
|
404
|
+
// that never changes the condition's inputs converges to the non-exiting-loop stop.
|
|
405
|
+
function lowerWhileStatement(statement: ts.WhileStatement, context: FunctionContext): void {
|
|
406
|
+
requireBooleanCondition(statement.expression, context.checker)
|
|
407
|
+
|
|
408
|
+
const bindingsBeforeLoop = new Map(context.bindings)
|
|
409
|
+
const assigned = assignedSymbols([statement.expression, statement.statement], context.checker)
|
|
410
|
+
const carried = [...bindingsBeforeLoop.keys()].filter(symbol => assigned.has(symbol))
|
|
411
|
+
const header = createBlock(context, carried.length, addSite(context, statement))
|
|
412
|
+
terminate(context.currentBlock, {
|
|
413
|
+
kind: 'jump',
|
|
414
|
+
target: {block: header, arguments: carried.map(symbol => requiredBranchBinding(symbol, bindingsBeforeLoop))},
|
|
415
|
+
site: addSite(context, statement),
|
|
416
|
+
})
|
|
417
|
+
|
|
418
|
+
context.currentBlock = context.blocks[header]!
|
|
419
|
+
context.bindings = new Map(bindingsBeforeLoop)
|
|
420
|
+
for (let index = 0; index < carried.length; index++) {
|
|
421
|
+
context.bindings.set(carried[index]!, context.currentBlock.parameters[index]!)
|
|
422
|
+
}
|
|
423
|
+
const conditionBindings = new Map(context.bindings)
|
|
424
|
+
const body = createBlock(context)
|
|
425
|
+
const exit = createBlock(context)
|
|
426
|
+
lowerBranchingCondition(statement.expression, body, exit, context)
|
|
427
|
+
|
|
428
|
+
context.currentBlock = context.blocks[body]!
|
|
429
|
+
context.bindings = new Map(conditionBindings)
|
|
430
|
+
context.loops.push({header, carried, advance: () => []})
|
|
431
|
+
lowerStatement(statement.statement, context)
|
|
432
|
+
context.loops.pop()
|
|
433
|
+
if (context.currentBlock.terminator == null) {
|
|
434
|
+
terminate(context.currentBlock, {
|
|
435
|
+
kind: 'jump',
|
|
436
|
+
target: {block: header, arguments: carried.map(symbol => requiredBranchBinding(symbol, context.bindings))},
|
|
437
|
+
site: addSite(context, statement),
|
|
438
|
+
})
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
context.currentBlock = context.blocks[exit]!
|
|
442
|
+
context.bindings = conditionBindings
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// `continue` ends the current path the way `return` does, except the jump targets the
|
|
446
|
+
// innermost loop's header instead of leaving the function. The loop's advance step runs
|
|
447
|
+
// first (a for loop's incrementor, the for-of counter bump), then the carried bindings
|
|
448
|
+
// are read at their current values — exactly what the normal body-end back edge does.
|
|
449
|
+
function lowerContinueStatement(statement: ts.ContinueStatement, context: FunctionContext): void {
|
|
450
|
+
if (statement.label != null) {
|
|
451
|
+
throw unsupported(statement, {kind: 'statementForm', syntax: 'ContinueStatement with a label'})
|
|
452
|
+
}
|
|
453
|
+
const loop = context.loops[context.loops.length - 1]
|
|
454
|
+
// TypeScript already rejects continue outside a loop; the guard keeps lowering total if
|
|
455
|
+
// such a file ever reaches this point.
|
|
456
|
+
if (loop == null) throw unsupported(statement, {kind: 'statementForm', syntax: 'ContinueStatement'})
|
|
457
|
+
const extra = loop.advance(context)
|
|
458
|
+
terminate(context.currentBlock, {
|
|
459
|
+
kind: 'jump',
|
|
460
|
+
target: {
|
|
461
|
+
block: loop.header,
|
|
462
|
+
arguments: [...loop.carried.map(symbol => requiredBranchBinding(symbol, context.bindings)), ...extra],
|
|
463
|
+
},
|
|
464
|
+
site: addSite(context, statement),
|
|
465
|
+
})
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function lowerBranch(
|
|
469
|
+
statement: ts.Statement,
|
|
470
|
+
block: BlockID,
|
|
471
|
+
bindings: Map<ts.Symbol, ValueID>,
|
|
472
|
+
context: FunctionContext,
|
|
473
|
+
): {block: MutableBlock; bindings: Map<ts.Symbol, ValueID>} {
|
|
474
|
+
context.currentBlock = context.blocks[block]!
|
|
475
|
+
context.bindings = new Map(bindings)
|
|
476
|
+
lowerStatement(statement, context)
|
|
477
|
+
return {block: context.currentBlock, bindings: context.bindings}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function lowerVariableDeclarationList(declarations: ts.VariableDeclarationList, context: FunctionContext): void {
|
|
481
|
+
for (const declaration of declarations.declarations) {
|
|
482
|
+
// `const {pos, dest} = config` lowers to one read of the source and one property read
|
|
483
|
+
// per name. Only plain shorthand or renamed identifier elements — no defaults, no rest.
|
|
484
|
+
if (ts.isObjectBindingPattern(declaration.name) && declaration.initializer != null) {
|
|
485
|
+
const source = lowerExpression(declaration.initializer, context)
|
|
486
|
+
for (const element of declaration.name.elements) {
|
|
487
|
+
if (!ts.isIdentifier(element.name) || element.dotDotDotToken != null || element.initializer != null) {
|
|
488
|
+
throw unsupported(element, {kind: 'variableDeclarationShape'})
|
|
489
|
+
}
|
|
490
|
+
const property = element.propertyName == null
|
|
491
|
+
? element.name.text
|
|
492
|
+
: ts.isIdentifier(element.propertyName)
|
|
493
|
+
? element.propertyName.text
|
|
494
|
+
: null
|
|
495
|
+
if (property == null) throw unsupported(element, {kind: 'variableDeclarationShape'})
|
|
496
|
+
const elementType = context.checker.getTypeAtLocation(element.name)
|
|
497
|
+
const sourceType = context.checker.getTypeAtLocation(declaration.initializer)
|
|
498
|
+
const propertySymbol = context.checker.getPropertyOfType(sourceType, property)
|
|
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
|
+
}
|
|
506
|
+
if (valueKind(elementType, context.checker) == null) {
|
|
507
|
+
throw unsupported(element, {kind: 'valueType', typeText: context.checker.typeToString(elementType)})
|
|
508
|
+
}
|
|
509
|
+
const value = addInstruction(context, element, {kind: 'property', object: source, property})
|
|
510
|
+
context.bindings.set(requiredSymbol(element.name, context.checker), value)
|
|
511
|
+
}
|
|
512
|
+
continue
|
|
513
|
+
}
|
|
514
|
+
if (!ts.isIdentifier(declaration.name) || declaration.initializer == null) {
|
|
515
|
+
throw unsupported(declaration, {kind: 'variableDeclarationShape'})
|
|
516
|
+
}
|
|
517
|
+
const value = lowerExpression(declaration.initializer, context)
|
|
518
|
+
// A variable whose declared type mixes kinds, e.g. `let u: unknown = 5` later reassigned
|
|
519
|
+
// to a boolean, lets branches rebind it to different kinds that would meet at the
|
|
520
|
+
// engine's block join instead of stopping here. The check runs after the initializer
|
|
521
|
+
// lowers, so an unsupported construct inside the initializer keeps its own more precise
|
|
522
|
+
// site (a ternary mixing kinds reports the ternary, not the whole declaration).
|
|
523
|
+
const declaredType = context.checker.getTypeAtLocation(declaration.name)
|
|
524
|
+
const declaredValueKind = valueKind(declaredType, context.checker)
|
|
525
|
+
if (declaredValueKind == null) {
|
|
526
|
+
throw unsupported(declaration.type ?? declaration.name, {
|
|
527
|
+
kind: 'valueType',
|
|
528
|
+
typeText: context.checker.typeToString(declaredType),
|
|
529
|
+
})
|
|
530
|
+
}
|
|
531
|
+
// An opaque-declared binding (`let u: unknown = 5`) erases its stored value: later
|
|
532
|
+
// branches may write other kinds, and opaque ⊔ opaque joins where number ⊔ boolean
|
|
533
|
+
// would crash. The initializer still lowered above, so its constructs stay vetted.
|
|
534
|
+
const stored = declaredValueKind === 'opaque'
|
|
535
|
+
? addInstruction(context, declaration, {kind: 'opaqueConstant'})
|
|
536
|
+
: value
|
|
537
|
+
context.bindings.set(requiredSymbol(declaration.name, context.checker), stored)
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function assignedSymbols(nodes: ts.Node[], checker: ts.TypeChecker): Set<ts.Symbol> {
|
|
542
|
+
const symbols = new Set<ts.Symbol>()
|
|
543
|
+
const visit = (node: ts.Node): void => {
|
|
544
|
+
if (ts.isFunctionLike(node)) return
|
|
545
|
+
// Shares the lowering's recognizer, so a form that lowers an assignment is carried
|
|
546
|
+
// across loop back edges by construction.
|
|
547
|
+
const assignment = identifierAssignment(node)
|
|
548
|
+
if (assignment != null) symbols.add(requiredSymbol(assignment.target, checker))
|
|
549
|
+
ts.forEachChild(node, visit)
|
|
550
|
+
}
|
|
551
|
+
for (const node of nodes) visit(node)
|
|
552
|
+
return symbols
|
|
553
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import * as ts from 'typescript'
|
|
2
|
+
|
|
3
|
+
type StaticAnnotationRole = 'requirement' | 'assertion'
|
|
4
|
+
|
|
5
|
+
export type StaticAnnotation =
|
|
6
|
+
| {kind: 'valid'; call: ts.CallExpression; role: StaticAnnotationRole; condition: ts.Expression}
|
|
7
|
+
| {
|
|
8
|
+
kind: 'invalid'
|
|
9
|
+
call: ts.CallExpression
|
|
10
|
+
role: StaticAnnotationRole
|
|
11
|
+
node: ts.Node
|
|
12
|
+
problem: 'argumentCount' | 'position' | 'optionalCall'
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type StaticAnnotationScan = {
|
|
16
|
+
functions: StaticAnnotation[][]
|
|
17
|
+
outsideTopLevelFunctions: ts.CallExpression[]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function scanStaticAnnotations(
|
|
21
|
+
sourceFile: ts.SourceFile,
|
|
22
|
+
declarations: ts.FunctionDeclaration[],
|
|
23
|
+
checker: ts.TypeChecker,
|
|
24
|
+
): StaticAnnotationScan {
|
|
25
|
+
const ownerIndex = new Map<ts.FunctionDeclaration, number>(
|
|
26
|
+
declarations.map((declaration, index) => [declaration, index]),
|
|
27
|
+
)
|
|
28
|
+
const callsByFunction = declarations.map(() => [] as ts.CallExpression[])
|
|
29
|
+
const outsideTopLevelFunctions: ts.CallExpression[] = []
|
|
30
|
+
|
|
31
|
+
const visit = (node: ts.Node): void => {
|
|
32
|
+
if (ts.isCallExpression(node) && isStaticIntent(node, checker)) {
|
|
33
|
+
const owner = ts.findAncestor(node, ts.isFunctionLike)
|
|
34
|
+
const index = owner != null && ts.isFunctionDeclaration(owner) ? ownerIndex.get(owner) : undefined
|
|
35
|
+
if (index == null) outsideTopLevelFunctions.push(node)
|
|
36
|
+
else callsByFunction[index]!.push(node)
|
|
37
|
+
}
|
|
38
|
+
ts.forEachChild(node, visit)
|
|
39
|
+
}
|
|
40
|
+
visit(sourceFile)
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
functions: declarations.map((declaration, index) => {
|
|
44
|
+
const calls = callsByFunction[index]!
|
|
45
|
+
const callSet = new Set(calls)
|
|
46
|
+
const leading = new Set<ts.CallExpression>()
|
|
47
|
+
if (declaration.body != null) {
|
|
48
|
+
for (const statement of declaration.body.statements) {
|
|
49
|
+
if (!ts.isExpressionStatement(statement) || !ts.isCallExpression(statement.expression)
|
|
50
|
+
|| !callSet.has(statement.expression)) break
|
|
51
|
+
leading.add(statement.expression)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return calls.map(call => annotationForCall(call, leading.has(call) ? 'requirement' : 'assertion'))
|
|
55
|
+
}),
|
|
56
|
+
outsideTopLevelFunctions,
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function annotationForCall(call: ts.CallExpression, role: StaticAnnotationRole): StaticAnnotation {
|
|
61
|
+
if (call.arguments.length !== 1) {
|
|
62
|
+
return {kind: 'invalid', call, role, node: call, problem: 'argumentCount'}
|
|
63
|
+
}
|
|
64
|
+
if (!ts.isExpressionStatement(call.parent) || call.parent.expression !== call) {
|
|
65
|
+
return {kind: 'invalid', call, role, node: call, problem: 'position'}
|
|
66
|
+
}
|
|
67
|
+
const callee = call.expression
|
|
68
|
+
if (call.questionDotToken != null
|
|
69
|
+
|| (ts.isPropertyAccessExpression(callee) && callee.questionDotToken != null)) {
|
|
70
|
+
return {kind: 'invalid', call, role, node: call, problem: 'optionalCall'}
|
|
71
|
+
}
|
|
72
|
+
return {kind: 'valid', call, role, condition: call.arguments[0]!}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function isStaticIntent(
|
|
76
|
+
call: ts.CallExpression,
|
|
77
|
+
checker: ts.TypeChecker,
|
|
78
|
+
): boolean {
|
|
79
|
+
if (!ts.isPropertyAccessExpression(call.expression)) return false
|
|
80
|
+
const access = call.expression
|
|
81
|
+
if (!ts.isIdentifier(access.expression)
|
|
82
|
+
|| access.expression.text !== 'console'
|
|
83
|
+
|| access.name.text !== 'assert') return false
|
|
84
|
+
const resolved = checker.getSymbolAtLocation(access.expression)
|
|
85
|
+
const globalConsole = checker.resolveName('console', undefined, ts.SymbolFlags.Value, false)
|
|
86
|
+
return resolved != null && resolved === globalConsole
|
|
87
|
+
}
|