@chenglou/freerange 0.0.1 → 0.0.3

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.
@@ -420,7 +420,10 @@ export function lowerExpression(expression: ts.Expression, context: FunctionCont
420
420
  arguments_.push(lowerParameterDefault(default_, parameter.initializer, context))
421
421
  continue
422
422
  }
423
- if (parameter.questionToken != null) {
423
+ const callableParameter = callee.signature?.declaration?.parameters[index]
424
+ if (callableParameter != null
425
+ && ts.isParameter(callableParameter)
426
+ && callableParameter.questionToken != null) {
424
427
  arguments_.push(addInstruction(context, parameter, {kind: 'nullishConstant', sentinel: 'undefined'}))
425
428
  continue
426
429
  }
@@ -452,7 +455,12 @@ export function lowerExpression(expression: ts.Expression, context: FunctionCont
452
455
  context,
453
456
  ))
454
457
  }
455
- return addInstruction(context, current, {kind: 'call', function: callee.id, arguments: arguments_})
458
+ return addInstruction(context, current, {
459
+ kind: 'call',
460
+ function: callee.id,
461
+ arguments: arguments_,
462
+ binding: callee.binding,
463
+ })
456
464
  }
457
465
  if (ts.isPropertyAccessExpression(current.expression)) {
458
466
  const platformCall = current.arguments.length === 0 ? platformFact(current.expression, true, context.checker) : null
@@ -882,12 +890,10 @@ export function compoundAssignmentOperator(kind: ts.SyntaxKind): Extract<Instruc
882
890
  }
883
891
  }
884
892
 
885
- // The shared value-producing branch shape: branch on the condition, lower each arm in its
886
- // own block, and join at a continuation whose single parameter carries the result. Arms are
887
- // provably assignment-free assignments lower only through lowerStatementExpression so
888
- // no bindings can change across the arms and the join needs no binding merge. Ternaries and
889
- // the logical operators are the two consumers; lowerIfStatement stays separate (no result
890
- // value, arms may terminate, and assignments are allowed there).
893
+ // The shared value-producing branch shape: lower each arm in its own block, then join at
894
+ // a continuation whose single parameter carries the result. Arms are assignment-free, so
895
+ // the join needs no binding merge. One entry accepts an existing condition value; the other
896
+ // keeps a compound condition split into branches so each check can narrow later operands.
891
897
  function lowerValueBranch(
892
898
  node: ts.Expression,
893
899
  condition: ValueID,
@@ -904,6 +910,44 @@ function lowerValueBranch(
904
910
  whenFalse: {block: whenFalse, arguments: []},
905
911
  site: addSite(context, node),
906
912
  })
913
+ return joinValueBranches(
914
+ node,
915
+ whenTrue,
916
+ whenFalse,
917
+ lowerTrueArm,
918
+ lowerFalseArm,
919
+ context,
920
+ )
921
+ }
922
+
923
+ function lowerBranchingValue(
924
+ node: ts.Expression,
925
+ condition: ts.Expression,
926
+ lowerTrueArm: () => ValueID,
927
+ lowerFalseArm: () => ValueID,
928
+ context: FunctionContext,
929
+ ): ValueID {
930
+ const whenTrue = createBlock(context)
931
+ const whenFalse = createBlock(context)
932
+ lowerBranchingCondition(condition, whenTrue, whenFalse, context)
933
+ return joinValueBranches(
934
+ node,
935
+ whenTrue,
936
+ whenFalse,
937
+ lowerTrueArm,
938
+ lowerFalseArm,
939
+ context,
940
+ )
941
+ }
942
+
943
+ function joinValueBranches(
944
+ node: ts.Expression,
945
+ whenTrue: number,
946
+ whenFalse: number,
947
+ lowerTrueArm: () => ValueID,
948
+ lowerFalseArm: () => ValueID,
949
+ context: FunctionContext,
950
+ ): ValueID {
907
951
  context.currentBlock = context.blocks[whenTrue]!
908
952
  const trueValue = lowerTrueArm()
909
953
  const trueBlock = context.currentBlock
@@ -937,28 +981,13 @@ function lowerConditionalExpression(expression: ts.ConditionalExpression, contex
937
981
  // condition as one boolean expression (the previous shape) hid the conjuncts behind a
938
982
  // joined block parameter no branch refinement could see through; a conversion pass on
939
983
  // the owner's repo caught the asymmetry.
940
- const whenTrue = createBlock(context)
941
- const whenFalse = createBlock(context)
942
- lowerBranchingCondition(expression.condition, whenTrue, whenFalse, context)
943
- context.currentBlock = context.blocks[whenTrue]!
944
- const trueValue = lowerExpression(expression.whenTrue, context)
945
- const trueBlock = context.currentBlock
946
- context.currentBlock = context.blocks[whenFalse]!
947
- const falseValue = lowerExpression(expression.whenFalse, context)
948
- const falseBlock = context.currentBlock
949
- const continuation = createBlock(context, 1)
950
- terminate(trueBlock, {
951
- kind: 'jump',
952
- target: {block: continuation, arguments: [trueValue]},
953
- site: addSite(context, expression),
954
- })
955
- terminate(falseBlock, {
956
- kind: 'jump',
957
- target: {block: continuation, arguments: [falseValue]},
958
- site: addSite(context, expression),
959
- })
960
- context.currentBlock = context.blocks[continuation]!
961
- return context.currentBlock.parameters[0]!
984
+ return lowerBranchingValue(
985
+ expression,
986
+ expression.condition,
987
+ () => lowerExpression(expression.whenTrue, context),
988
+ () => lowerExpression(expression.whenFalse, context),
989
+ context,
990
+ )
962
991
  }
963
992
 
964
993
  // `a && b` evaluates b only when a is true and yields false otherwise; `a || b` mirrors it —
@@ -1017,10 +1046,9 @@ function lowerLogicalExpression(expression: ts.BinaryExpression, context: Functi
1017
1046
  requireBooleanCondition(expression.left, context.checker)
1018
1047
  requireBooleanCondition(expression.right, context.checker)
1019
1048
  const isAnd = expression.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken
1020
- const condition = lowerExpression(expression.left, context)
1021
- return lowerValueBranch(
1049
+ return lowerBranchingValue(
1022
1050
  expression,
1023
- condition,
1051
+ expression.left,
1024
1052
  () => isAnd
1025
1053
  ? lowerExpression(expression.right, context)
1026
1054
  : addInstruction(context, expression, {kind: 'booleanConstant', value: true}),
@@ -0,0 +1,64 @@
1
+ import * as ts from 'typescript'
2
+
3
+ export type TopLevelFunctionNode =
4
+ | ts.FunctionDeclaration
5
+ | ts.ArrowFunction
6
+ | ts.FunctionExpression
7
+
8
+ export type TopLevelFunctionUnit = {
9
+ name: ts.Identifier
10
+ declaration: TopLevelFunctionNode
11
+ initializer: ts.VariableDeclaration | null
12
+ }
13
+
14
+ export function callableSignature(
15
+ unit: TopLevelFunctionUnit,
16
+ checker: ts.TypeChecker,
17
+ ): ts.Signature | null {
18
+ if (unit.initializer == null) {
19
+ return checker.getSignatureFromDeclaration(unit.declaration) ?? null
20
+ }
21
+ const signatures = checker.getSignaturesOfType(
22
+ checker.getTypeAtLocation(unit.name),
23
+ ts.SignatureKind.Call,
24
+ )
25
+ if (signatures.length !== 1) return null
26
+ const signature = signatures[0]!
27
+ const signatureDeclaration = signature.declaration
28
+ if (signatureDeclaration == null
29
+ || signature.thisParameter != null
30
+ || signature.parameters.length !== unit.declaration.parameters.length
31
+ || signatureDeclaration.parameters.some(parameter =>
32
+ !ts.isParameter(parameter) || parameter.dotDotDotToken != null)) {
33
+ return null
34
+ }
35
+ return signature
36
+ }
37
+
38
+ export function topLevelFunctionUnits(sourceFile: ts.SourceFile): TopLevelFunctionUnit[] {
39
+ const units: TopLevelFunctionUnit[] = []
40
+ for (const statement of sourceFile.statements) {
41
+ if (ts.isFunctionDeclaration(statement) && statement.name != null) {
42
+ units.push({name: statement.name, declaration: statement, initializer: null})
43
+ continue
44
+ }
45
+ if (!ts.isVariableStatement(statement)
46
+ || (statement.declarationList.flags & ts.NodeFlags.Const) === 0) continue
47
+ for (const initializer of statement.declarationList.declarations) {
48
+ if (!ts.isIdentifier(initializer.name) || initializer.initializer == null) continue
49
+ const declaration = directFunctionExpression(initializer.initializer)
50
+ if (declaration != null) {
51
+ units.push({name: initializer.name, declaration, initializer})
52
+ }
53
+ }
54
+ }
55
+ return units
56
+ }
57
+
58
+ export function directFunctionExpression(
59
+ expression: ts.Expression,
60
+ ): ts.ArrowFunction | ts.FunctionExpression | null {
61
+ return ts.isArrowFunction(expression) || ts.isFunctionExpression(expression)
62
+ ? expression
63
+ : null
64
+ }
@@ -18,6 +18,7 @@ import {numericLiteralValue} from './literals.ts'
18
18
  import {declaredOnlyInDeclarationFiles} from './platform.ts'
19
19
  import {addInstruction, addSite, createFunctionContext, LoweringStop, restoreLowering, sealBlocks, snapshotLowering, terminate, type FunctionContext, type TopLevelFunction} from './context.ts'
20
20
  import {lowerExpression, nonMissingUnionMembers, tagLiteralValues, taggedUnionProperty, valueKind} from './expression.ts'
21
+ import {directFunctionExpression} from './function-unit.ts'
21
22
  import {lowerStatement} from './statements.ts'
22
23
 
23
24
  export type ModuleScan = {
@@ -45,9 +46,16 @@ export function scanModuleBindings(sourceFile: ts.SourceFile, checker: ts.TypeCh
45
46
  // the statement itself is skipped when the initializer reaches it, and functions
46
47
  // reading the name are rejected as unknown identifiers.
47
48
  if ((statement.declarationList.flags & (ts.NodeFlags.Let | ts.NodeFlags.Const)) === 0) continue
49
+ const isConst = (statement.declarationList.flags & ts.NodeFlags.Const) !== 0
48
50
  for (const declarator of statement.declarationList.declarations) {
49
51
  if (ts.isIdentifier(declarator.name)) {
50
- register(declarator.name, declaredCategory(declarator.name, checker))
52
+ register(
53
+ declarator.name,
54
+ isConst && declarator.initializer != null
55
+ && directFunctionExpression(declarator.initializer) != null
56
+ ? {kind: 'function'}
57
+ : declaredCategory(declarator.name, checker),
58
+ )
51
59
  continue
52
60
  }
53
61
  // `const {cols} = gridSize` at the top level: each destructured name is its own
@@ -206,7 +214,7 @@ export function lowerModuleInitializer(
206
214
  if (skippedAtTopLevel(statement)) continue
207
215
  const recovery = snapshotLowering(context)
208
216
  try {
209
- assertAccepted(statement)
217
+ assertAccepted(statement, true)
210
218
  if (ts.isVariableStatement(statement)) {
211
219
  lowerTopLevelDeclarations(statement, context, scan)
212
220
  continue
@@ -354,6 +362,12 @@ function lowerTopLevelDeclarations(statement: ts.VariableStatement, context: Fun
354
362
  const symbol = context.checker.getSymbolAtLocation(declarator.name)
355
363
  const binding = symbol == null ? undefined : scan.bindingsBySymbol.get(symbol)
356
364
  if (binding == null) throw new LoweringStop(declarator, {kind: 'variableDeclarationShape'})
365
+ if ((statement.declarationList.flags & ts.NodeFlags.Const) !== 0
366
+ && directFunctionExpression(declarator.initializer) != null) {
367
+ const marker = addInstruction(context, declarator.initializer, {kind: 'opaqueConstant'})
368
+ addInstruction(context, declarator, {kind: 'moduleWrite', binding, value: marker})
369
+ continue
370
+ }
357
371
  const value = lowerExpression(declarator.initializer, context)
358
372
  addInstruction(context, declarator, {kind: 'moduleWrite', binding, value})
359
373
  }
@@ -361,8 +375,9 @@ function lowerTopLevelDeclarations(statement: ts.VariableStatement, context: Fun
361
375
 
362
376
  function skippedAtTopLevel(statement: ts.Statement): boolean {
363
377
  // `export {alreadyDeclaredName}` and import declarations create bindings but run nothing.
364
- // Only NAMED function declarations pass: those become program.functions entries, so
365
- // unsupported code inside them keeps the fully-analyzed publish gate honest. An
378
+ // Named declarations pass here: those become program.functions entries, so unsupported
379
+ // code inside them keeps the fully-analyzed publish gate honest. Const-bound functions
380
+ // remain variable statements and lower above as function-initialization markers. An
366
381
  // anonymous `export default function` has no name to collect under, so it falls through
367
382
  // to ordinary statement lowering, which records it as an initializer skip — otherwise
368
383
  // its body would be runtime code invisible to every gate.
@@ -741,6 +756,7 @@ function demote(bindings: ModuleBindingIR[], binding: ModuleBindingID): void {
741
756
  break
742
757
  }
743
758
  case 'kind':
759
+ case 'function':
744
760
  case 'import':
745
761
  case 'opaque':
746
762
  break
@@ -47,6 +47,9 @@ const catalog: PlatformEntry[] = [
47
47
  // resolution); Date.now is an integer count of milliseconds.
48
48
  {path: ['performance', 'now'], call: true, fact: {lower: 0, upper: Number.MAX_VALUE, integer: false}},
49
49
  {path: ['Date', 'now'], call: true, fact: {lower: 0, upper: Number.MAX_VALUE, integer: true}},
50
+ // Math.random returns one of JavaScript's discrete floating-point numbers below 1.
51
+ // Writing the predecessor of 1 keeps the exact closed range without adding open bounds.
52
+ {path: ['Math', 'random'], call: true, fact: {lower: 0, upper: 0.9999999999999999, integer: false}},
50
53
  ]
51
54
 
52
55
  // Matches a property chain rooted at a global whose symbol resolves into a declaration
@@ -4,7 +4,8 @@ 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'
@@ -12,10 +13,7 @@ import {lowerStatements} from './statements.ts'
12
13
 
13
14
  export function lowerSource(checked: CheckedSource, baseDirectory: string = process.cwd()): ProgramIR {
14
15
  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
- }
16
+ const declarations = topLevelFunctionUnits(sourceFile)
19
17
  const staticScan = scanStaticAnnotations(sourceFile, declarations, checker)
20
18
  const recordStaticAnnotationIssues = (sites: SourceSpan[]): ProgramIR['staticAnnotationIssues'] =>
21
19
  staticScan.outsideTopLevelFunctions.map(call => {
@@ -34,9 +32,9 @@ export function lowerSource(checked: CheckedSource, baseDirectory: string = proc
34
32
  baseDirectory,
35
33
  lineStarts: [...sourceFile.getLineStarts()],
36
34
  sites,
37
- functions: declarations.map((declaration, index) => ({
35
+ functions: declarations.map((unit, index) => ({
38
36
  kind: 'unsupported',
39
- name: declaration.name!.text,
37
+ name: unit.name.text,
40
38
  hasStaticAnnotations: staticScan.functions[index]!.length > 0,
41
39
  site: 0,
42
40
  reason,
@@ -62,21 +60,33 @@ export function lowerSource(checked: CheckedSource, baseDirectory: string = proc
62
60
  return rejectFile(nodeSpan(sourceFile, evalNode), {kind: 'evalInFile'})
63
61
  }
64
62
  const functionsBySymbol = new Map<ts.Symbol, TopLevelFunction>()
63
+ const scan = scanModuleBindings(sourceFile, checker)
64
+ const topLevelFunctions: TopLevelFunction[] = []
65
65
  for (let index = 0; index < declarations.length; index++) {
66
- const declaration = declarations[index]!
66
+ const unit = declarations[index]!
67
67
  // This loop runs outside the per-function catch below, so a missing symbol here is an
68
68
  // invariant crash, not a recorded reason: a declaration name that type-checked always
69
69
  // 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})
70
+ const symbol = checker.getSymbolAtLocation(unit.name)
71
+ if (symbol == null) throw new Error(`Function ${unit.name.text} has no TypeScript symbol`)
72
+ const binding = scan.bindingsBySymbol.get(symbol)
73
+ if (unit.initializer != null && binding == null) {
74
+ throw new Error(`Const-bound function ${unit.name.text} has no module binding`)
75
+ }
76
+ const fn: TopLevelFunction = {
77
+ ...unit,
78
+ id: index,
79
+ binding: unit.initializer == null ? null : binding!,
80
+ signature: callableSignature(unit, checker),
81
+ }
82
+ topLevelFunctions.push(fn)
83
+ functionsBySymbol.set(symbol, fn)
73
84
  }
74
- const scan = scanModuleBindings(sourceFile, checker)
75
85
  const sites: SourceSpan[] = []
76
86
  const staticAnnotationIssues = recordStaticAnnotationIssues(sites)
77
87
  const functions: FunctionLowering[] = []
78
- for (let index = 0; index < declarations.length; index++) {
79
- const declaration = declarations[index]!
88
+ for (let index = 0; index < topLevelFunctions.length; index++) {
89
+ const declaration = topLevelFunctions[index]!
80
90
  const staticAnnotations = staticScan.functions[index]!
81
91
  // A failed function lowering discards the half-built FunctionContext wholesale; only
82
92
  // the name, annotation presence, offending node's site, and tagged reason survive.
@@ -87,7 +97,7 @@ export function lowerSource(checked: CheckedSource, baseDirectory: string = proc
87
97
  sites.push(nodeSpan(sourceFile, error.node))
88
98
  functions.push({
89
99
  kind: 'unsupported',
90
- name: declaration.name!.text,
100
+ name: declaration.name.text,
91
101
  hasStaticAnnotations: staticAnnotations.length > 0,
92
102
  site: sites.length - 1,
93
103
  reason: error.reason,
@@ -109,7 +119,7 @@ export function lowerSource(checked: CheckedSource, baseDirectory: string = proc
109
119
  }
110
120
 
111
121
  function lowerFunction(
112
- declaration: ts.FunctionDeclaration,
122
+ unit: TopLevelFunction,
113
123
  staticAnnotations: StaticAnnotation[],
114
124
  sourceFile: ts.SourceFile,
115
125
  checker: ts.TypeChecker,
@@ -117,6 +127,7 @@ function lowerFunction(
117
127
  scan: ModuleScan,
118
128
  sites: SourceSpan[],
119
129
  ): FunctionIR {
130
+ const {declaration} = unit
120
131
  for (const annotation of staticAnnotations) {
121
132
  if (annotation.kind === 'invalid') {
122
133
  throw unsupported(annotation.node, {kind: 'staticAssertionForm', problem: annotation.problem})
@@ -126,18 +137,24 @@ function lowerFunction(
126
137
  // An async body returns a Promise and a generator returns an iterator; lowering either
127
138
  // as if it ran synchronously would publish the body's values as the caller-visible
128
139
  // result. Rejected wholesale.
129
- if (declaration.asteriskToken != null || declaration.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword) === true) {
140
+ if ((!ts.isArrowFunction(declaration) && declaration.asteriskToken != null)
141
+ || declaration.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword) === true) {
130
142
  throw unsupported(declaration, {kind: 'asyncOrGeneratorFunction'})
131
143
  }
132
144
  assertAccepted(declaration)
133
- const signature = checker.getSignatureFromDeclaration(declaration)
145
+ const signature = unit.signature
146
+ if (signature == null) {
147
+ throw unsupported(unit.name, {
148
+ kind: unit.initializer == null ? 'functionWithoutSignature' : 'constFunctionSignature',
149
+ })
150
+ }
134
151
  // A type predicate (`shape is Circle`, `asserts x`) is the checker taking the author's
135
152
  // word: callers' narrowing then exposes properties the analysis cannot confirm the
136
153
  // value carries. Rejecting the declaring function stops every caller at the call.
137
- if (signature != null && checker.getTypePredicateOfSignature(signature) != null) {
154
+ if (checker.getTypePredicateOfSignature(signature) != null) {
138
155
  throw unsupported(declaration, {kind: 'typePredicate'})
139
156
  }
140
- const returnType = functionReturnType(declaration, checker)
157
+ const returnType = checker.getReturnTypeOfSignature(signature)
141
158
  // `never` counts as returning nothing: the idiomatic annotation for an always-throwing
142
159
  // helper (`function fail(code: number): never`), whose paths all end in throw — the
143
160
  // always-throws analysis and the calleeAlwaysThrows caller stop handle the rest.
@@ -154,16 +171,21 @@ function lowerFunction(
154
171
  scan.bindingsBySymbol,
155
172
  sites,
156
173
  staticAnnotations,
174
+ returnsVoid,
157
175
  )
158
176
  const entry = context.currentBlock
159
- for (const parameter of declaration.parameters) {
177
+ for (let parameterIndex = 0; parameterIndex < declaration.parameters.length; parameterIndex++) {
178
+ const parameter = declaration.parameters[parameterIndex]!
179
+ const parameterType = unit.initializer == null
180
+ ? checker.getTypeAtLocation(parameter)
181
+ : checker.getTypeOfSymbolAtLocation(signature.parameters[parameterIndex]!, signature.declaration!)
160
182
  // `function area({width, height}: Size)` lowers as a synthetic record parameter plus
161
183
  // one property read per name — the same classification named parameters use, the
162
184
  // same reads body destructuring uses. The report metadata below keeps the local
163
185
  // names, so a condition says `width` rather than `{width, height}.width`. Defaults
164
186
  // and rest inside the pattern stay out, like the body form.
165
187
  if (ts.isObjectBindingPattern(parameter.name)) {
166
- const type = lowerParameterType(parameter, checker)
188
+ const type = lowerParameterType(parameter, parameterType, checker)
167
189
  // The pattern text becomes the parameter's report name; a pattern the author wrapped
168
190
  // across source lines would otherwise break the one-fact-per-line report format
169
191
  // (`assumes: {` and orphan fragments — a corpus census caught eight of these).
@@ -201,7 +223,7 @@ function lowerFunction(
201
223
  if (parameter.dotDotDotToken != null) {
202
224
  throw unsupported(parameter, {kind: 'parameterType', typeText: `...${checker.typeToString(checker.getTypeAtLocation(parameter))}`, optionalOrRestTuple: false})
203
225
  }
204
- let type = lowerParameterType(parameter, checker)
226
+ let type = lowerParameterType(parameter, parameterType, checker)
205
227
  // A default value applies whenever a caller omits the argument. Literal defaults can
206
228
  // be represented exactly and checked against the declared assumptions: `zoom: number
207
229
  // = 5` supplies a finite number. Anything else — `= Infinity`, `= readConfig()` —
@@ -218,7 +240,16 @@ function lowerFunction(
218
240
  context.parameters.push({value, name: parameter.name.text, type, site: addSite(context, parameter), bindings: null})
219
241
  }
220
242
  lowerFiniteInputRequirements(context)
221
- lowerStatements(declaration.body.statements, context)
243
+ if (ts.isBlock(declaration.body)) {
244
+ lowerStatements(declaration.body.statements, context)
245
+ } else {
246
+ const value = lowerExpression(declaration.body, context)
247
+ terminate(context.currentBlock, {
248
+ kind: 'return',
249
+ value: returnsVoid ? null : value,
250
+ site: addSite(context, declaration.body),
251
+ })
252
+ }
222
253
  if (context.currentBlock.terminator == null) {
223
254
  if (!returnsVoid) {
224
255
  // A non-void path reaching the end without a return is a per-path STOP, not a
@@ -234,12 +265,12 @@ function lowerFunction(
234
265
  }
235
266
  return {
236
267
  kind: 'lowered',
237
- name: declaration.name!.text,
268
+ name: unit.name.text,
238
269
  assertions: context.assertions,
239
270
  parameters: context.parameters,
240
271
  returnPropertyNames: declaredRecordReturnNames(returnType, checker),
241
272
  entry: 0,
242
- blocks: sealBlocks(context.blocks, declaration.name!.text),
273
+ blocks: sealBlocks(context.blocks, unit.name.text),
243
274
  }
244
275
  }
245
276
 
@@ -287,11 +318,14 @@ function declaredRecordReturnNames(returnType: ts.Type, checker: ts.TypeChecker)
287
318
  return null
288
319
  }
289
320
 
290
- function lowerParameterType(parameter: ts.ParameterDeclaration, checker: ts.TypeChecker): DeclaredKind {
321
+ function lowerParameterType(
322
+ parameter: ts.ParameterDeclaration,
323
+ type: ts.Type,
324
+ checker: ts.TypeChecker,
325
+ ): DeclaredKind {
291
326
  // The same recursive classification module bindings use: numbers, booleans, records
292
327
  // (opaque leaves included — an id: string property is carried, not rejected), nullable
293
328
  // wrappers, arrays, tuples, and bare opaque (a plain string parameter).
294
- const type = checker.getTypeAtLocation(parameter)
295
329
  const declared = declaredKind(type, checker, [])
296
330
  if (declared == null) {
297
331
  throw unsupported(parameter, {
@@ -302,9 +336,3 @@ function lowerParameterType(parameter: ts.ParameterDeclaration, checker: ts.Type
302
336
  }
303
337
  return declared
304
338
  }
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
- }
@@ -30,17 +30,12 @@ export function lowerStatement(statement: ts.Statement, context: FunctionContext
30
30
  }
31
31
  if (ts.isReturnStatement(statement)) {
32
32
  let value = statement.expression == null ? null : lowerExpression(statement.expression, context)
33
+ if (context.returnsVoid) value = null
33
34
  // A bare `return` in a function that returns a value IS `return undefined` — the
34
35
  // common early exit in a `T | undefined` function. In void functions (and the module
35
36
  // 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
- }
37
+ if (value == null && !context.returnsVoid) {
38
+ value = addInstruction(context, statement, {kind: 'nullishConstant', sentinel: 'undefined'})
44
39
  }
45
40
  terminate(context.currentBlock, {kind: 'return', value, site: addSite(context, statement)})
46
41
  return
@@ -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.
@@ -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