@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chenglou/freerange",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "description": "Static numeric range analysis for TypeScript",
5
5
  "license": "MIT",
6
6
  "repository": {
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 function declarations']
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
+ }
@@ -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 {addPrecondition, constantRequirementStatus, createExpressionContext, staticRequirement} from '../requirements/infer.ts'
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' || demoted.has(index)) return null
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 (holdsMutableStructure(binding.category.declaredKind) && !fullyAnalyzed) return null
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
- parameterIdentityKeys?: string[]
311
- identityNamespace?: string
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.parameterIdentityKeys,
338
- seed.identityNamespace ?? fn.name,
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
- parameterIdentityKeys: string[],
369
- identityNamespace: string,
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, parameterIdentityKeys, identityNamespace},
390
+ {valueFacts, parameterIdentities, identityOwner},
384
391
  ).evaluation
385
392
  },
386
393
  }
@@ -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: string}
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: string; array: string}
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: string; array: string}
29
+ | {kind: 'validIndex'; index: ValueIdentity; array: ValueIdentity}
26
30
 
27
- export function hasNonzeroFact(facts: ValueFact[], value: string): boolean {
28
- return facts.some(fact => fact.kind === 'nonzero' && fact.value === value)
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: string,
35
- array: string,
39
+ index: ValueIdentity,
40
+ array: ValueIdentity,
36
41
  ): boolean {
37
- return facts.some(fact => fact.kind === kind && fact.index === index && fact.array === array)
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 !== right.index || left.array !== right.array) return null
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') return left.value === right.value
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 === right.index && left.array === right.array
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 {
@@ -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
- parameterIdentityKeys: string[],
71
- identityNamespace: string,
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 indexKey = canonicalValueKey(instruction.index, context.expressionContext)
214
- const arrayKey = canonicalValueKey(instruction.array, context.expressionContext)
215
- const assumedValid = hasIndexFact(state.valueFacts, 'validIndex', indexKey, arrayKey)
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(state.valueFacts, 'belowLength', indexKey, arrayKey))
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, {kind: 'validIndex', index: indexKey, array: arrayKey})
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 writable
334
- // category is single-kind: value/kind writes are type-checked against the declared
335
- // number, boolean, or record shape.
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' identity keys. The same small fact list
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 argumentKeys = instruction.arguments.map(id => canonicalValueKey(id, context.expressionContext))
542
- const calleeNamespace = `${context.expressionContext.identityNamespace}call:${instruction.function}:${instruction.site}/`
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
- argumentKeys,
551
- calleeNamespace,
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
- !valueFactUsesNamespace(fact, calleeNamespace))
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 valueFactUsesNamespace(fact: ValueFact, namespace: string): boolean {
668
- const marker = `v:${namespace}`
669
- if (fact.kind === 'nonzero') return fact.value.includes(marker)
670
- return fact.index.includes(marker) || fact.array.includes(marker)
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: canonicalValueKey(comparison.left, expressionContext),
1118
- array: canonicalValueKey(rightProducer.array, expressionContext),
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: canonicalValueKey(comparison.right, expressionContext),
1128
- array: canonicalValueKey(leftProducer.array, expressionContext),
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 key = canonicalValueKey(id, expressionContext)
1216
- if (state.valueFacts.some(fact => fact.kind === 'validIndex' && fact.index === key)) {
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, key)) result = excludePointFrom(result, constantNumber(0))
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, {kind: 'nonzero', value: canonicalValueKey(value, expressionContext)})
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 {
@@ -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 & {kind: 'call'; function: FunctionID; arguments: ValueID[]})
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
@@ -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 declaration and once per top-level statement of the module initializer; a
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
@@ -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 declaration and its index in ProgramIR.functions. The call arm
21
- // keeps the declaration so omitted optional and literal-defaulted arguments can be filled
22
- // before emitting the fixed-arity call instruction.
23
- export type TopLevelFunction = {
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
- declaration: ts.FunctionDeclaration
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
  }