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