@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/package.json
CHANGED
package/src/audit.ts
CHANGED
|
@@ -286,7 +286,7 @@ export function createFileAudit({program, analysis}: {program: ProgramIR; analys
|
|
|
286
286
|
|
|
287
287
|
function formatAuditCoverage(coverage: AuditCoverage): string {
|
|
288
288
|
const parts = coverage.functions === 0
|
|
289
|
-
? ['no named
|
|
289
|
+
? ['no named top-level functions']
|
|
290
290
|
: [`${coverage.analyzed}/${coverage.functions} functions fully analyzed`]
|
|
291
291
|
if (coverage.partial > 0) parts.push(`${coverage.partial} partially supported`)
|
|
292
292
|
if (coverage.unsupported > 0) parts.push(`${coverage.unsupported} unsupported`)
|
|
@@ -405,6 +405,7 @@ function guidesForUnsupportedReason(reason: UnsupportedReason): RefactorGuideID[
|
|
|
405
405
|
case 'unknownIdentifier':
|
|
406
406
|
case 'missingSymbol':
|
|
407
407
|
case 'functionWithoutSignature':
|
|
408
|
+
case 'constFunctionSignature':
|
|
408
409
|
case 'functionWithoutBody':
|
|
409
410
|
case 'destructuredParameter':
|
|
410
411
|
case 'parameterType':
|
|
@@ -414,10 +415,8 @@ function guidesForUnsupportedReason(reason: UnsupportedReason): RefactorGuideID[
|
|
|
414
415
|
case 'computedPropertyName':
|
|
415
416
|
case 'objectSpread':
|
|
416
417
|
case 'asyncOrGeneratorFunction':
|
|
417
|
-
case 'typePredicate':
|
|
418
418
|
case 'protoProperty':
|
|
419
419
|
case 'enumMemberRead':
|
|
420
|
-
case 'prototypeMemberRead':
|
|
421
420
|
case 'binaryOperator':
|
|
422
421
|
case 'callWithFewerArguments':
|
|
423
422
|
case 'callWithMoreArguments':
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type {ValueID} from '../ir/ids.ts'
|
|
2
|
+
|
|
3
|
+
export type ValueIdentityOwner = {
|
|
4
|
+
kind: 'valueIdentityOwner'
|
|
5
|
+
parent: ValueIdentityOwner | null
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type ValueIdentity =
|
|
9
|
+
| {kind: 'local'; owner: ValueIdentityOwner; value: ValueID}
|
|
10
|
+
| {kind: 'property'; object: ValueIdentity; property: string}
|
|
11
|
+
| {kind: 'arrayIndex'; array: ValueIdentity; index: ValueIdentity}
|
|
12
|
+
|
|
13
|
+
export function createValueIdentityOwner(
|
|
14
|
+
parent: ValueIdentityOwner | null = null,
|
|
15
|
+
): ValueIdentityOwner {
|
|
16
|
+
return {kind: 'valueIdentityOwner', parent}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function sameValueIdentity(left: ValueIdentity, right: ValueIdentity): boolean {
|
|
20
|
+
if (left === right) return true
|
|
21
|
+
if (left.kind !== right.kind) return false
|
|
22
|
+
switch (left.kind) {
|
|
23
|
+
case 'local':
|
|
24
|
+
return right.kind === 'local'
|
|
25
|
+
&& left.owner === right.owner
|
|
26
|
+
&& left.value === right.value
|
|
27
|
+
case 'property':
|
|
28
|
+
return right.kind === 'property'
|
|
29
|
+
&& left.property === right.property
|
|
30
|
+
&& sameValueIdentity(left.object, right.object)
|
|
31
|
+
case 'arrayIndex':
|
|
32
|
+
return right.kind === 'arrayIndex'
|
|
33
|
+
&& sameValueIdentity(left.array, right.array)
|
|
34
|
+
&& sameValueIdentity(left.index, right.index)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function valueIdentityUsesOwner(
|
|
39
|
+
identity: ValueIdentity,
|
|
40
|
+
owner: ValueIdentityOwner,
|
|
41
|
+
): boolean {
|
|
42
|
+
switch (identity.kind) {
|
|
43
|
+
case 'local': {
|
|
44
|
+
let current: ValueIdentityOwner | null = identity.owner
|
|
45
|
+
while (current != null) {
|
|
46
|
+
if (current === owner) return true
|
|
47
|
+
current = current.parent
|
|
48
|
+
}
|
|
49
|
+
return false
|
|
50
|
+
}
|
|
51
|
+
case 'property': return valueIdentityUsesOwner(identity.object, owner)
|
|
52
|
+
case 'arrayIndex':
|
|
53
|
+
return valueIdentityUsesOwner(identity.array, owner)
|
|
54
|
+
|| valueIdentityUsesOwner(identity.index, owner)
|
|
55
|
+
}
|
|
56
|
+
}
|
package/src/engine/analyze.ts
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import {constantNumber} from '../domain/number.ts'
|
|
2
2
|
import {joinValues, type AbstractValue} from '../domain/value.ts'
|
|
3
|
+
import type {ValueIdentity, ValueIdentityOwner} from '../domain/value-identity.ts'
|
|
3
4
|
import type {BlockID, FunctionID, ModuleBindingID, SiteID} from '../ir/ids.ts'
|
|
4
5
|
import {functionUsage, transitiveModuleBindings} from '../ir/function-usage.ts'
|
|
5
6
|
import {finiteInputExpression, finiteInputs} from '../ir/finite-inputs.ts'
|
|
6
7
|
import type {EdgeIR} from '../ir/instructions.ts'
|
|
7
8
|
import {declaredKindOf, declaredKindValue, holdsMutableStructure, type FunctionIR, type ProgramIR} from '../ir/program.ts'
|
|
8
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
addPrecondition,
|
|
11
|
+
constantRequirementStatus,
|
|
12
|
+
createExpressionContext,
|
|
13
|
+
staticRequirement,
|
|
14
|
+
} from '../requirements/infer.ts'
|
|
9
15
|
import type {BoundsAssumption, InferredPrecondition, NumericExpression} from '../requirements/model.ts'
|
|
10
16
|
import {
|
|
11
17
|
completedEvaluation,
|
|
@@ -63,7 +69,6 @@ export function analyzeProgram(program: ProgramIR): ProgramAnalysis {
|
|
|
63
69
|
initializerState,
|
|
64
70
|
program,
|
|
65
71
|
[],
|
|
66
|
-
{identityNamespace: 'module/'},
|
|
67
72
|
)
|
|
68
73
|
const moduleValues = publishedModuleValues(program, initializer.run, initializer.evaluation)
|
|
69
74
|
const functionEntrySharedState = seedModuleSlots(program, moduleValues)
|
|
@@ -98,7 +103,6 @@ export function analyzeProgram(program: ProgramIR): ProgramAnalysis {
|
|
|
98
103
|
[],
|
|
99
104
|
{
|
|
100
105
|
boundsAssumptions: moduleReads[functionID]!.size > 0 ? initializerBounds : [],
|
|
101
|
-
identityNamespace: `function:${functionID}/`,
|
|
102
106
|
},
|
|
103
107
|
)
|
|
104
108
|
functions.push(publishedAnalysis(fn, evaluation))
|
|
@@ -252,10 +256,13 @@ function publishedModuleValues(
|
|
|
252
256
|
&& program.functions.every(lowered => lowered.kind === 'lowered')
|
|
253
257
|
|
|
254
258
|
return program.moduleBindings.map((binding, index) => {
|
|
255
|
-
if (binding.category.kind !== 'value'
|
|
259
|
+
if ((binding.category.kind !== 'value' && binding.category.kind !== 'function')
|
|
260
|
+
|| demoted.has(index)) return null
|
|
256
261
|
// holdsMutableStructure, not a top-level tag check: a `number[] | null` binding is
|
|
257
262
|
// nullish at the top level yet the array inside is exactly as alias-mutable.
|
|
258
|
-
if (
|
|
263
|
+
if (binding.category.kind === 'value'
|
|
264
|
+
&& holdsMutableStructure(binding.category.declaredKind)
|
|
265
|
+
&& !fullyAnalyzed) return null
|
|
259
266
|
const slot = end?.[index]
|
|
260
267
|
return slot ?? null
|
|
261
268
|
})
|
|
@@ -307,8 +314,8 @@ type EvaluationRun = {
|
|
|
307
314
|
type EvaluationSeed = {
|
|
308
315
|
boundsAssumptions?: BoundsAssumption[]
|
|
309
316
|
valueFacts?: ValueFact[]
|
|
310
|
-
|
|
311
|
-
|
|
317
|
+
parameterIdentities?: ValueIdentity[]
|
|
318
|
+
identityOwner?: ValueIdentityOwner
|
|
312
319
|
}
|
|
313
320
|
|
|
314
321
|
function runEvaluation(
|
|
@@ -334,8 +341,8 @@ function runEvaluation(
|
|
|
334
341
|
const expressionContext = createExpressionContext(
|
|
335
342
|
fn,
|
|
336
343
|
argumentExpressions,
|
|
337
|
-
seed.
|
|
338
|
-
seed.
|
|
344
|
+
seed.parameterIdentities,
|
|
345
|
+
seed.identityOwner,
|
|
339
346
|
)
|
|
340
347
|
const preconditions: InferredPrecondition[] = []
|
|
341
348
|
const boundsAssumptions: BoundsAssumption[] = [...(seed.boundsAssumptions ?? [])]
|
|
@@ -365,8 +372,8 @@ function runEvaluation(
|
|
|
365
372
|
calleeState: SharedState,
|
|
366
373
|
stack: FunctionID[],
|
|
367
374
|
valueFacts: ValueFact[],
|
|
368
|
-
|
|
369
|
-
|
|
375
|
+
parameterIdentities: ValueIdentity[],
|
|
376
|
+
identityOwner: ValueIdentityOwner,
|
|
370
377
|
) => {
|
|
371
378
|
const calleeFn = program.functions[callee]
|
|
372
379
|
if (calleeFn == null) throw new Error(`Unknown function ${callee}`)
|
|
@@ -380,7 +387,7 @@ function runEvaluation(
|
|
|
380
387
|
calleeState,
|
|
381
388
|
program,
|
|
382
389
|
stack,
|
|
383
|
-
{valueFacts,
|
|
390
|
+
{valueFacts, parameterIdentities, identityOwner},
|
|
384
391
|
).evaluation
|
|
385
392
|
},
|
|
386
393
|
}
|
package/src/engine/state.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type {AbstractValue} from '../domain/value.ts'
|
|
2
2
|
import {joinValues, sameValues, widenValue} from '../domain/value.ts'
|
|
3
|
+
import {
|
|
4
|
+
sameValueIdentity,
|
|
5
|
+
type ValueIdentity,
|
|
6
|
+
} from '../domain/value-identity.ts'
|
|
3
7
|
|
|
4
8
|
// Indexed by ModuleBindingID, fixed length per program. Flows through calls, so a
|
|
5
9
|
// callee's module writes are visible to the caller after a completed call. Null means the
|
|
@@ -17,24 +21,28 @@ export type ExecutionState = {
|
|
|
17
21
|
}
|
|
18
22
|
|
|
19
23
|
export type ValueFact =
|
|
20
|
-
| {kind: 'nonzero'; value:
|
|
24
|
+
| {kind: 'nonzero'; value: ValueIdentity}
|
|
21
25
|
// The strict `index < array.length` half of a bounds guard. The index's own abstract
|
|
22
26
|
// number must still prove integer, non-NaN, and nonnegative.
|
|
23
|
-
| {kind: 'belowLength'; index:
|
|
27
|
+
| {kind: 'belowLength'; index: ValueIdentity; array: ValueIdentity}
|
|
24
28
|
// A requirement or assumption for an asserted read proves the complete condition.
|
|
25
|
-
| {kind: 'validIndex'; index:
|
|
29
|
+
| {kind: 'validIndex'; index: ValueIdentity; array: ValueIdentity}
|
|
26
30
|
|
|
27
|
-
export function hasNonzeroFact(facts: ValueFact[], value:
|
|
28
|
-
return facts.some(fact =>
|
|
31
|
+
export function hasNonzeroFact(facts: ValueFact[], value: ValueIdentity): boolean {
|
|
32
|
+
return facts.some(fact =>
|
|
33
|
+
fact.kind === 'nonzero' && sameValueIdentity(fact.value, value))
|
|
29
34
|
}
|
|
30
35
|
|
|
31
36
|
export function hasIndexFact(
|
|
32
37
|
facts: ValueFact[],
|
|
33
38
|
kind: 'belowLength' | 'validIndex',
|
|
34
|
-
index:
|
|
35
|
-
array:
|
|
39
|
+
index: ValueIdentity,
|
|
40
|
+
array: ValueIdentity,
|
|
36
41
|
): boolean {
|
|
37
|
-
return facts.some(fact =>
|
|
42
|
+
return facts.some(fact =>
|
|
43
|
+
fact.kind === kind
|
|
44
|
+
&& sameValueIdentity(fact.index, index)
|
|
45
|
+
&& sameValueIdentity(fact.array, array))
|
|
38
46
|
}
|
|
39
47
|
|
|
40
48
|
export function addValueFact(facts: ValueFact[], candidate: ValueFact): void {
|
|
@@ -55,15 +63,19 @@ export function intersectValueFacts(left: ValueFact[], right: ValueFact[]): Valu
|
|
|
55
63
|
function intersectValueFact(left: ValueFact, right: ValueFact): ValueFact | null {
|
|
56
64
|
if (sameValueFact(left, right)) return left
|
|
57
65
|
if (left.kind === 'nonzero' || right.kind === 'nonzero') return null
|
|
58
|
-
if (left.index
|
|
66
|
+
if (!sameValueIdentity(left.index, right.index)
|
|
67
|
+
|| !sameValueIdentity(left.array, right.array)) return null
|
|
59
68
|
return {kind: 'belowLength', index: left.index, array: left.array}
|
|
60
69
|
}
|
|
61
70
|
|
|
62
71
|
function sameValueFact(left: ValueFact, right: ValueFact): boolean {
|
|
63
72
|
if (left.kind !== right.kind) return false
|
|
64
|
-
if (left.kind === 'nonzero' && right.kind === 'nonzero')
|
|
73
|
+
if (left.kind === 'nonzero' && right.kind === 'nonzero') {
|
|
74
|
+
return sameValueIdentity(left.value, right.value)
|
|
75
|
+
}
|
|
65
76
|
if (left.kind === 'nonzero' || right.kind === 'nonzero') return false
|
|
66
|
-
return left.index
|
|
77
|
+
return sameValueIdentity(left.index, right.index)
|
|
78
|
+
&& sameValueIdentity(left.array, right.array)
|
|
67
79
|
}
|
|
68
80
|
|
|
69
81
|
export function emptySharedState(moduleCount: number): SharedState {
|
package/src/engine/transfer.ts
CHANGED
|
@@ -34,14 +34,21 @@ import {
|
|
|
34
34
|
type AbstractValue,
|
|
35
35
|
type TaggedVariant,
|
|
36
36
|
} from '../domain/value.ts'
|
|
37
|
+
import {
|
|
38
|
+
createValueIdentityOwner,
|
|
39
|
+
sameValueIdentity,
|
|
40
|
+
valueIdentityUsesOwner,
|
|
41
|
+
type ValueIdentity,
|
|
42
|
+
type ValueIdentityOwner,
|
|
43
|
+
} from '../domain/value-identity.ts'
|
|
37
44
|
import type {FunctionID, SiteID, ValueID} from '../ir/ids.ts'
|
|
38
45
|
import type {ComparisonOperator, InstructionIR} from '../ir/instructions.ts'
|
|
39
46
|
import {coveringKindValue, declaredKindOf, type DeclaredKind, type ProgramIR} from '../ir/program.ts'
|
|
40
47
|
import {
|
|
41
48
|
addPrecondition,
|
|
49
|
+
canonicalValueIdentity,
|
|
42
50
|
constantRequirementStatus,
|
|
43
51
|
peelNonzero,
|
|
44
|
-
canonicalValueKey,
|
|
45
52
|
numericExpression,
|
|
46
53
|
resolveStoredValue,
|
|
47
54
|
sameRuntimeValue,
|
|
@@ -67,8 +74,8 @@ type EvaluateFunction = (
|
|
|
67
74
|
sharedState: SharedState,
|
|
68
75
|
callStack: FunctionID[],
|
|
69
76
|
valueFacts: ValueFact[],
|
|
70
|
-
|
|
71
|
-
|
|
77
|
+
parameterIdentities: ValueIdentity[],
|
|
78
|
+
identityOwner: ValueIdentityOwner,
|
|
72
79
|
) => FunctionEvaluation
|
|
73
80
|
|
|
74
81
|
export type TransferContext = {
|
|
@@ -210,16 +217,32 @@ function evaluateInstructionKinded(
|
|
|
210
217
|
? tupleElement(sequence, index)
|
|
211
218
|
: sequence.element
|
|
212
219
|
const length = sequence.kind === 'tuple' ? constantNumber(sequence.elements.length) : sequence.length
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
220
|
+
const indexIdentity = canonicalValueIdentity(
|
|
221
|
+
instruction.index,
|
|
222
|
+
context.expressionContext,
|
|
223
|
+
)
|
|
224
|
+
const arrayIdentity = canonicalValueIdentity(
|
|
225
|
+
instruction.array,
|
|
226
|
+
context.expressionContext,
|
|
227
|
+
)
|
|
228
|
+
const assumedValid = hasIndexFact(
|
|
229
|
+
state.valueFacts,
|
|
230
|
+
'validIndex',
|
|
231
|
+
indexIdentity,
|
|
232
|
+
arrayIdentity,
|
|
233
|
+
)
|
|
216
234
|
// Three proofs of in-bounds: a complete prior requirement, intervals, or the strict
|
|
217
235
|
// below-length half from a guard combined with the index's own integer/nonnegative
|
|
218
236
|
// facts. A for-of loop reaches the same third proof through its generated guard.
|
|
219
237
|
const inBounds = assumedValid
|
|
220
238
|
|| (index.integer && !index.mayBeNaN && index.lower >= 0 && index.upper < length.lower)
|
|
221
239
|
|| (index.integer && !index.mayBeNaN && index.lower >= 0
|
|
222
|
-
&& hasIndexFact(
|
|
240
|
+
&& hasIndexFact(
|
|
241
|
+
state.valueFacts,
|
|
242
|
+
'belowLength',
|
|
243
|
+
indexIdentity,
|
|
244
|
+
arrayIdentity,
|
|
245
|
+
))
|
|
223
246
|
// A provably out-of-bounds read: for the asserted form the assertion lied; for the
|
|
224
247
|
// bare form the value is exactly undefined. An empty sequence is the special case
|
|
225
248
|
// where every read is out of bounds.
|
|
@@ -263,7 +286,11 @@ function evaluateInstructionKinded(
|
|
|
263
286
|
state, instruction.index, validIndexNumber(index),
|
|
264
287
|
context.expressionContext.instructionByValue,
|
|
265
288
|
)
|
|
266
|
-
addValueFact(state.valueFacts, {
|
|
289
|
+
addValueFact(state.valueFacts, {
|
|
290
|
+
kind: 'validIndex',
|
|
291
|
+
index: indexIdentity,
|
|
292
|
+
array: arrayIdentity,
|
|
293
|
+
})
|
|
267
294
|
}
|
|
268
295
|
return passthroughValue(element)
|
|
269
296
|
}
|
|
@@ -330,9 +357,9 @@ function evaluateInstructionKinded(
|
|
|
330
357
|
// An opaque binding's declared type spans value kinds (e.g. `unknown`), so two paths
|
|
331
358
|
// could put a number and a boolean in one slot and meet at a join, which only handles
|
|
332
359
|
// matching kinds. Reads of opaque bindings stop regardless, so the slot stays
|
|
333
|
-
// uninitialized instead of holding a value nothing may consume. Every other
|
|
334
|
-
// category is single-kind
|
|
335
|
-
//
|
|
360
|
+
// uninitialized instead of holding a value nothing may consume. Every other value
|
|
361
|
+
// category is single-kind; a function binding stores only the internal marker that
|
|
362
|
+
// its top-level initializer has run.
|
|
336
363
|
if (binding.category.kind !== 'opaque') {
|
|
337
364
|
state.shared[instruction.binding] = assigned
|
|
338
365
|
}
|
|
@@ -525,6 +552,15 @@ function evaluateInstructionKinded(
|
|
|
525
552
|
return computedNumber(maximumNumbers(operands), operands, instruction.site)
|
|
526
553
|
}
|
|
527
554
|
case 'call': {
|
|
555
|
+
if (instruction.binding != null && state.shared[instruction.binding] == null) {
|
|
556
|
+
return {
|
|
557
|
+
kind: 'stop',
|
|
558
|
+
stop: {
|
|
559
|
+
site: instruction.site,
|
|
560
|
+
reason: {kind: 'moduleRead', binding: instruction.binding},
|
|
561
|
+
},
|
|
562
|
+
}
|
|
563
|
+
}
|
|
528
564
|
const callee = context.program.functions[instruction.function]
|
|
529
565
|
if (callee == null) throw new Error(`Unknown function ${instruction.function}`)
|
|
530
566
|
if (callee.kind === 'unsupported') {
|
|
@@ -535,11 +571,14 @@ function evaluateInstructionKinded(
|
|
|
535
571
|
}
|
|
536
572
|
const arguments_ = instruction.arguments.map(id => requiredValue(state, id))
|
|
537
573
|
const argumentExpressions = instruction.arguments.map(id => numericExpression(id, context.expressionContext))
|
|
538
|
-
// Parameters use their caller arguments'
|
|
574
|
+
// Parameters use their caller arguments' identities. The same small fact list
|
|
539
575
|
// therefore works inside the callee and comes back after every normal return, so a
|
|
540
576
|
// requirement established by a completed helper call applies below that call.
|
|
541
|
-
const
|
|
542
|
-
|
|
577
|
+
const argumentIdentities = instruction.arguments.map(id =>
|
|
578
|
+
canonicalValueIdentity(id, context.expressionContext))
|
|
579
|
+
const calleeOwner = createValueIdentityOwner(
|
|
580
|
+
context.expressionContext.identityOwner,
|
|
581
|
+
)
|
|
543
582
|
const evaluation = context.evaluateFunction(
|
|
544
583
|
instruction.function,
|
|
545
584
|
arguments_,
|
|
@@ -547,8 +586,8 @@ function evaluateInstructionKinded(
|
|
|
547
586
|
state.shared,
|
|
548
587
|
context.callStack,
|
|
549
588
|
state.valueFacts,
|
|
550
|
-
|
|
551
|
-
|
|
589
|
+
argumentIdentities,
|
|
590
|
+
calleeOwner,
|
|
552
591
|
)
|
|
553
592
|
// A partial callee's result is discarded wholesale: the callee ran on a clone, and
|
|
554
593
|
// state.shared is assigned only on the complete path below, so a partial callee's
|
|
@@ -576,7 +615,7 @@ function evaluateInstructionKinded(
|
|
|
576
615
|
}
|
|
577
616
|
state.shared = completed.sharedState
|
|
578
617
|
state.valueFacts = completed.valueFacts.filter(fact =>
|
|
579
|
-
!
|
|
618
|
+
!valueFactUsesOwner(fact, calleeOwner))
|
|
580
619
|
for (let index = 0; index < callee.parameters.length; index++) {
|
|
581
620
|
refineFiniteCallArgument(
|
|
582
621
|
state,
|
|
@@ -664,10 +703,13 @@ export function addBoundsAssumption(assumptions: BoundsAssumption[], candidate:
|
|
|
664
703
|
}
|
|
665
704
|
}
|
|
666
705
|
|
|
667
|
-
function
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
706
|
+
function valueFactUsesOwner(
|
|
707
|
+
fact: ValueFact,
|
|
708
|
+
owner: ValueIdentityOwner,
|
|
709
|
+
): boolean {
|
|
710
|
+
if (fact.kind === 'nonzero') return valueIdentityUsesOwner(fact.value, owner)
|
|
711
|
+
return valueIdentityUsesOwner(fact.index, owner)
|
|
712
|
+
|| valueIdentityUsesOwner(fact.array, owner)
|
|
671
713
|
}
|
|
672
714
|
|
|
673
715
|
function sentinelsAdmit(sentinels: 'null' | 'undefined' | 'both', sentinel: 'null' | 'undefined'): boolean {
|
|
@@ -1114,8 +1156,8 @@ function refineComparison(
|
|
|
1114
1156
|
if (rightProducer?.kind === 'arrayLength') {
|
|
1115
1157
|
addValueFact(result.valueFacts, {
|
|
1116
1158
|
kind: 'belowLength',
|
|
1117
|
-
index:
|
|
1118
|
-
array:
|
|
1159
|
+
index: canonicalValueIdentity(comparison.left, expressionContext),
|
|
1160
|
+
array: canonicalValueIdentity(rightProducer.array, expressionContext),
|
|
1119
1161
|
})
|
|
1120
1162
|
}
|
|
1121
1163
|
}
|
|
@@ -1124,8 +1166,8 @@ function refineComparison(
|
|
|
1124
1166
|
if (leftProducer?.kind === 'arrayLength') {
|
|
1125
1167
|
addValueFact(result.valueFacts, {
|
|
1126
1168
|
kind: 'belowLength',
|
|
1127
|
-
index:
|
|
1128
|
-
array:
|
|
1169
|
+
index: canonicalValueIdentity(comparison.right, expressionContext),
|
|
1170
|
+
array: canonicalValueIdentity(leftProducer.array, expressionContext),
|
|
1129
1171
|
})
|
|
1130
1172
|
}
|
|
1131
1173
|
}
|
|
@@ -1212,11 +1254,15 @@ function numberWithFacts(
|
|
|
1212
1254
|
const held = state.values[id]
|
|
1213
1255
|
if (held?.kind !== 'number') return null
|
|
1214
1256
|
let result = held
|
|
1215
|
-
const
|
|
1216
|
-
if (state.valueFacts.some(fact =>
|
|
1257
|
+
const identity = canonicalValueIdentity(id, expressionContext)
|
|
1258
|
+
if (state.valueFacts.some(fact =>
|
|
1259
|
+
fact.kind === 'validIndex'
|
|
1260
|
+
&& sameValueIdentity(fact.index, identity))) {
|
|
1217
1261
|
result = validIndexNumber(result)
|
|
1218
1262
|
}
|
|
1219
|
-
if (hasNonzeroFact(state.valueFacts,
|
|
1263
|
+
if (hasNonzeroFact(state.valueFacts, identity)) {
|
|
1264
|
+
result = excludePointFrom(result, constantNumber(0))
|
|
1265
|
+
}
|
|
1220
1266
|
return result
|
|
1221
1267
|
}
|
|
1222
1268
|
|
|
@@ -1245,7 +1291,10 @@ function recordNonzeroValueFact(
|
|
|
1245
1291
|
value: ValueID,
|
|
1246
1292
|
expressionContext: ExpressionContext,
|
|
1247
1293
|
): void {
|
|
1248
|
-
addValueFact(state.valueFacts, {
|
|
1294
|
+
addValueFact(state.valueFacts, {
|
|
1295
|
+
kind: 'nonzero',
|
|
1296
|
+
value: canonicalValueIdentity(value, expressionContext),
|
|
1297
|
+
})
|
|
1249
1298
|
}
|
|
1250
1299
|
|
|
1251
1300
|
export function requiredBoolean(state: ExecutionState, id: ValueID): AbstractBoolean {
|
package/src/ir/instructions.ts
CHANGED
|
@@ -94,7 +94,13 @@ export type InstructionIR =
|
|
|
94
94
|
| (InstructionBase & {kind: 'staticRequire'; value: ValueID; purpose?: 'finiteInput'})
|
|
95
95
|
| (InstructionBase & {kind: 'staticAssert'; value: ValueID; assertion: number})
|
|
96
96
|
| (InstructionBase & {kind: 'minimum' | 'maximum'; values: ValueID[]})
|
|
97
|
-
| (InstructionBase & {
|
|
97
|
+
| (InstructionBase & {
|
|
98
|
+
kind: 'call'
|
|
99
|
+
function: FunctionID
|
|
100
|
+
arguments: ValueID[]
|
|
101
|
+
// A const-bound function is unavailable until its top-level initializer runs.
|
|
102
|
+
binding: ModuleBindingID | null
|
|
103
|
+
})
|
|
98
104
|
// tag is set when the literal's contextual type is a tagged union and the literal names
|
|
99
105
|
// its tag with a string literal — the engine then builds a single-variant union, so
|
|
100
106
|
// branches building different variants join per tag instead of dropping properties.
|
package/src/ir/program.ts
CHANGED
|
@@ -74,6 +74,9 @@ export type UnsupportedReason =
|
|
|
74
74
|
// user source is a shaky boundary, so the case is recorded rather than crashed on.
|
|
75
75
|
| {kind: 'missingSymbol'}
|
|
76
76
|
| {kind: 'functionWithoutSignature'}
|
|
77
|
+
// A const-bound function's public type is overloaded, has a rest/this parameter, or has
|
|
78
|
+
// a different parameter count from the arrow or function expression.
|
|
79
|
+
| {kind: 'constFunctionSignature'}
|
|
77
80
|
// Overload signatures and ambient declarations have no body to lower.
|
|
78
81
|
| {kind: 'functionWithoutBody'}
|
|
79
82
|
| {kind: 'destructuredParameter'}
|
|
@@ -94,11 +97,8 @@ export type UnsupportedReason =
|
|
|
94
97
|
| {kind: 'computedPropertyName'}
|
|
95
98
|
| {kind: 'objectSpread'}
|
|
96
99
|
| {kind: 'asyncOrGeneratorFunction'}
|
|
97
|
-
| {kind: 'typePredicate'}
|
|
98
100
|
| {kind: 'protoProperty'}
|
|
99
101
|
| {kind: 'enumMemberRead'}
|
|
100
|
-
// point.toString and friends: a prototype member the record value cannot answer.
|
|
101
|
-
| {kind: 'prototypeMemberRead'; property: string}
|
|
102
102
|
// e.g. '**', '>>', 'instanceof' in value position — the operators arithmetic and
|
|
103
103
|
// comparison lowering do not claim ('%' and '??' lower now and no longer land here)
|
|
104
104
|
| {kind: 'binaryOperator'; operator: string}
|
|
@@ -234,6 +234,9 @@ export type ModuleBindingCategory =
|
|
|
234
234
|
// literal is trusted exactly, without analyzing the exporting module; the soundness
|
|
235
235
|
// argument sits on importedCategory in src/lower/module.ts.
|
|
236
236
|
| {kind: 'importedConstant'; value: number}
|
|
237
|
+
// A directly assigned const arrow or function expression. Its slot carries only whether
|
|
238
|
+
// module initialization has reached the declaration; calls use the FunctionIR itself.
|
|
239
|
+
| {kind: 'function'}
|
|
237
240
|
// Every other declared type (unions with null, arrays, strings, functions, records with
|
|
238
241
|
// optional or unrepresentable properties). Reads stop.
|
|
239
242
|
| {kind: 'opaque'}
|
|
@@ -249,6 +252,7 @@ export function declaredKindOf(category: ModuleBindingCategory): DeclaredKind |
|
|
|
249
252
|
// An imported constant needs no declared-kind hedge: its slot is always seeded with
|
|
250
253
|
// the exact literal, so no assumes line and no havoc reset value apply.
|
|
251
254
|
case 'importedConstant':
|
|
255
|
+
case 'function':
|
|
252
256
|
case 'import':
|
|
253
257
|
case 'opaque':
|
|
254
258
|
return null
|
package/src/lower/accept.ts
CHANGED
|
@@ -6,10 +6,14 @@ import {unsupported} from './context.ts'
|
|
|
6
6
|
// immutable after construction) and `var` — checked before lowering ever sees the code.
|
|
7
7
|
// (`any`-typed values and type assertions used to be rejected here too; both are carried
|
|
8
8
|
// claim-free now — see valueKind's opaque arm and unwrap's assertion peeling.) Called once
|
|
9
|
-
// per function
|
|
9
|
+
// per analyzed top-level function and once per top-level statement of the module initializer; a
|
|
10
10
|
// violation throws LoweringStop and is caught like any other rejection.
|
|
11
|
-
export function assertAccepted(root: ts.Node): void {
|
|
11
|
+
export function assertAccepted(root: ts.Node, deferFunctionBodies: boolean = false): void {
|
|
12
12
|
const visit = (node: ts.Node): void => {
|
|
13
|
+
// Creating a nested function does not execute its body. The function gets its own
|
|
14
|
+
// acceptance check if it is a supported top-level unit; all other function values are
|
|
15
|
+
// rejected later where the surrounding lowering encounters them.
|
|
16
|
+
if (deferFunctionBodies && node !== root && ts.isFunctionLike(node)) return
|
|
13
17
|
// Type annotations hold no runtime values, so there is nothing to check inside them.
|
|
14
18
|
if (ts.isTypeNode(node)) return
|
|
15
19
|
// JSX never lowers (the expression catch-all names it), but its tag names and
|
package/src/lower/context.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
import type {InstructionIR, TerminatorIR} from '../ir/instructions.ts'
|
|
10
10
|
import {nodeSpan, type BlockIR, type FunctionIR, type SourceSpan, type UnsupportedReason} from '../ir/program.ts'
|
|
11
11
|
import type {StaticAnnotation} from './static-intrinsics.ts'
|
|
12
|
+
import type {TopLevelFunctionUnit} from './function-unit.ts'
|
|
12
13
|
|
|
13
14
|
export type MutableBlock = {
|
|
14
15
|
loopHeader: SiteID | null
|
|
@@ -17,17 +18,20 @@ export type MutableBlock = {
|
|
|
17
18
|
terminator: TerminatorIR | null
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
// A top-level function
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
|
|
21
|
+
// A top-level function and its index in ProgramIR.functions. The call arm keeps the
|
|
22
|
+
// declaration so omitted optional and literal-defaulted arguments can be filled before
|
|
23
|
+
// emitting the fixed-arity call instruction. binding is present for a const-bound
|
|
24
|
+
// function, whose initializer must run before the function can be called.
|
|
25
|
+
export type TopLevelFunction = TopLevelFunctionUnit & {
|
|
24
26
|
id: FunctionID
|
|
25
|
-
|
|
27
|
+
binding: ModuleBindingID | null
|
|
28
|
+
signature: ts.Signature | null
|
|
26
29
|
}
|
|
27
30
|
|
|
28
31
|
export type FunctionContext = {
|
|
29
32
|
sourceFile: ts.SourceFile
|
|
30
33
|
checker: ts.TypeChecker
|
|
34
|
+
program: ts.Program
|
|
31
35
|
functionsBySymbol: Map<ts.Symbol, TopLevelFunction>
|
|
32
36
|
moduleBindingsBySymbol: Map<ts.Symbol, ModuleBindingID>
|
|
33
37
|
staticAnnotations: Map<ts.CallExpression, StaticAnnotation>
|
|
@@ -40,6 +44,7 @@ export type FunctionContext = {
|
|
|
40
44
|
bindings: Map<ts.Symbol, ValueID>
|
|
41
45
|
parameters: FunctionIR['parameters']
|
|
42
46
|
assertions: FunctionIR['assertions']
|
|
47
|
+
returnsVoid: boolean
|
|
43
48
|
// Innermost-last stack of enclosing loops, consulted by `continue`. A continue runs the
|
|
44
49
|
// loop's advance step (a for loop's incrementor, the for-of counter bump; nothing for
|
|
45
50
|
// while), then jumps to the header carrying the loop's carried bindings plus whatever
|
|
@@ -56,15 +61,18 @@ export type LoopTarget = {
|
|
|
56
61
|
export function createFunctionContext(
|
|
57
62
|
sourceFile: ts.SourceFile,
|
|
58
63
|
checker: ts.TypeChecker,
|
|
64
|
+
program: ts.Program,
|
|
59
65
|
functionsBySymbol: Map<ts.Symbol, TopLevelFunction>,
|
|
60
66
|
moduleBindingsBySymbol: Map<ts.Symbol, ModuleBindingID>,
|
|
61
67
|
sites: SourceSpan[],
|
|
62
68
|
staticAnnotations: StaticAnnotation[] = [],
|
|
69
|
+
returnsVoid: boolean = true,
|
|
63
70
|
): FunctionContext {
|
|
64
71
|
const entry: MutableBlock = {loopHeader: null, parameters: [], instructions: [], terminator: null}
|
|
65
72
|
return {
|
|
66
73
|
sourceFile,
|
|
67
74
|
checker,
|
|
75
|
+
program,
|
|
68
76
|
functionsBySymbol,
|
|
69
77
|
moduleBindingsBySymbol,
|
|
70
78
|
staticAnnotations: new Map(staticAnnotations.map(annotation => [annotation.call, annotation])),
|
|
@@ -75,6 +83,7 @@ export function createFunctionContext(
|
|
|
75
83
|
bindings: new Map(),
|
|
76
84
|
parameters: [],
|
|
77
85
|
assertions: [],
|
|
86
|
+
returnsVoid,
|
|
78
87
|
loops: [],
|
|
79
88
|
}
|
|
80
89
|
}
|