@chenglou/freerange 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/LICENSE +21 -0
- package/README.md +242 -0
- package/fr.ts +33 -0
- package/package.json +39 -0
- package/src/analyze.ts +16 -0
- package/src/audit.ts +468 -0
- package/src/domain/number.ts +444 -0
- package/src/domain/value.ts +445 -0
- package/src/engine/analyze.ts +745 -0
- package/src/engine/outcome.ts +137 -0
- package/src/engine/state.ts +155 -0
- package/src/engine/transfer.ts +1723 -0
- package/src/index.ts +23 -0
- package/src/ir/finite-inputs.ts +51 -0
- package/src/ir/function-usage.ts +50 -0
- package/src/ir/ids.ts +10 -0
- package/src/ir/instructions.ts +164 -0
- package/src/ir/program.ts +446 -0
- package/src/lower/accept.ts +119 -0
- package/src/lower/context.ts +250 -0
- package/src/lower/expression.ts +1742 -0
- package/src/lower/literals.ts +84 -0
- package/src/lower/module.ts +748 -0
- package/src/lower/platform.ts +81 -0
- package/src/lower/program.ts +310 -0
- package/src/lower/statements.ts +553 -0
- package/src/lower/static-intrinsics.ts +87 -0
- package/src/project.ts +500 -0
- package/src/report/format-requirement.ts +110 -0
- package/src/report/index.ts +1132 -0
- package/src/requirements/infer.ts +334 -0
- package/src/requirements/model.ts +77 -0
- package/src/typescript/check.ts +56 -0
- package/src/typescript/diagnostics.ts +69 -0
- package/src/typescript/project.ts +101 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import * as ts from 'typescript'
|
|
2
|
+
|
|
3
|
+
// The platform catalog: numeric facts about browser globals, the analyzer's trusted base
|
|
4
|
+
// the way lib.dom.d.ts is the type system's. Each entry is a deliberate, written decision —
|
|
5
|
+
// a wrong range here poisons everything downstream — and the ranges deliberately include
|
|
6
|
+
// platform quirks (scroll positions go negative during Safari rubber-banding, so no scroll
|
|
7
|
+
// entry claims nonnegativity). A matched read produces a FRESH value within the range on
|
|
8
|
+
// every evaluation: these are reads of outside mutable state, so two reads of clientWidth
|
|
9
|
+
// may differ and must never be treated as equal.
|
|
10
|
+
//
|
|
11
|
+
// Only number-valued entries live here; a nullable API would need an explicit guard before
|
|
12
|
+
// its numeric result could use a catalog fact.
|
|
13
|
+
// (Related known gap for then: lib.dom types document.body as non-null, but it is null
|
|
14
|
+
// before the body parses — a script running in <head> can throw on the scroll entries.
|
|
15
|
+
// Partial correctness tolerates the throw; nullability work should revisit the entry.)
|
|
16
|
+
|
|
17
|
+
export type PlatformFact = {
|
|
18
|
+
lower: number
|
|
19
|
+
upper: number
|
|
20
|
+
integer: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type PlatformEntry = {
|
|
24
|
+
// Property path from a global root, e.g. ['document', 'documentElement', 'clientWidth'].
|
|
25
|
+
path: string[]
|
|
26
|
+
// Whether the final path element is called, e.g. performance.now().
|
|
27
|
+
call: boolean
|
|
28
|
+
fact: PlatformFact
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const anyFinite = {lower: -Number.MAX_VALUE, upper: Number.MAX_VALUE}
|
|
32
|
+
|
|
33
|
+
const catalog: PlatformEntry[] = [
|
|
34
|
+
// Element layout sizes are nonnegative integers (the spec rounds them).
|
|
35
|
+
{path: ['document', 'documentElement', 'clientWidth'], call: false, fact: {lower: 0, upper: Number.MAX_VALUE, integer: true}},
|
|
36
|
+
{path: ['document', 'documentElement', 'clientHeight'], call: false, fact: {lower: 0, upper: Number.MAX_VALUE, integer: true}},
|
|
37
|
+
// Window sizes can be fractional under browser zoom.
|
|
38
|
+
{path: ['window', 'innerWidth'], call: false, fact: {lower: 0, upper: Number.MAX_VALUE, integer: false}},
|
|
39
|
+
{path: ['window', 'innerHeight'], call: false, fact: {lower: 0, upper: Number.MAX_VALUE, integer: false}},
|
|
40
|
+
// Scroll positions are finite but NOT nonnegative: Safari rubber-banding reports negative
|
|
41
|
+
// values at the edges, and fractional values appear under zoom.
|
|
42
|
+
{path: ['window', 'scrollX'], call: false, fact: {...anyFinite, integer: false}},
|
|
43
|
+
{path: ['window', 'scrollY'], call: false, fact: {...anyFinite, integer: false}},
|
|
44
|
+
{path: ['document', 'body', 'scrollTop'], call: false, fact: {...anyFinite, integer: false}},
|
|
45
|
+
{path: ['document', 'body', 'scrollLeft'], call: false, fact: {...anyFinite, integer: false}},
|
|
46
|
+
// Monotonic clocks: finite, nonnegative, fractional (performance.now has sub-millisecond
|
|
47
|
+
// resolution); Date.now is an integer count of milliseconds.
|
|
48
|
+
{path: ['performance', 'now'], call: true, fact: {lower: 0, upper: Number.MAX_VALUE, integer: false}},
|
|
49
|
+
{path: ['Date', 'now'], call: true, fact: {lower: 0, upper: Number.MAX_VALUE, integer: true}},
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
// Matches a property chain rooted at a global whose symbol resolves into a declaration
|
|
53
|
+
// file — the same shadowing defense the Math dispatch uses, so a local variable named
|
|
54
|
+
// `document` never matches.
|
|
55
|
+
export function platformFact(expression: ts.Expression, call: boolean, checker: ts.TypeChecker): PlatformFact | null {
|
|
56
|
+
const parts: string[] = []
|
|
57
|
+
let current: ts.Expression = expression
|
|
58
|
+
while (ts.isPropertyAccessExpression(current)) {
|
|
59
|
+
parts.unshift(current.name.text)
|
|
60
|
+
current = current.expression
|
|
61
|
+
}
|
|
62
|
+
if (!ts.isIdentifier(current)) return null
|
|
63
|
+
parts.unshift(current.text)
|
|
64
|
+
const entry = catalog.find(candidate =>
|
|
65
|
+
candidate.call === call
|
|
66
|
+
&& candidate.path.length === parts.length
|
|
67
|
+
&& candidate.path.every((segment, index) => segment === parts[index]))
|
|
68
|
+
if (entry == null) return null
|
|
69
|
+
if (!declaredOnlyInDeclarationFiles(checker.getSymbolAtLocation(current))) return null
|
|
70
|
+
return entry.fact
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// The shadowing defense shared by every trusted-global dispatch (this catalog, `Math`,
|
|
74
|
+
// `Infinity`): the identifier counts as the real global only when every declaration of its
|
|
75
|
+
// symbol lives in a declaration file, so a local or module binding of the same name never
|
|
76
|
+
// matches.
|
|
77
|
+
export function declaredOnlyInDeclarationFiles(symbol: ts.Symbol | undefined): boolean {
|
|
78
|
+
const declarations = symbol?.declarations
|
|
79
|
+
if (declarations == null || declarations.length === 0) return false
|
|
80
|
+
return declarations.every(declaration => declaration.getSourceFile().isDeclarationFile)
|
|
81
|
+
}
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import * as ts from 'typescript'
|
|
2
|
+
import {finiteInputPaths} from '../ir/finite-inputs.ts'
|
|
3
|
+
import {moduleInitializerName, nodeSpan, type DeclaredKind, type FunctionIR, type FunctionLowering, type ProgramIR, type SourceSpan, type UnsupportedReason} from '../ir/program.ts'
|
|
4
|
+
import type {CheckedSource} from '../typescript/check.ts'
|
|
5
|
+
import {assertAccepted, evalMention, typeCheckSuppressionMention} from './accept.ts'
|
|
6
|
+
import {addInstructionAtSite, addSite, createFunctionContext, LoweringStop, requiredSymbol, sealBlocks, terminate, unsupported, type MutableBlock, type TopLevelFunction} from './context.ts'
|
|
7
|
+
import {valueKind} from './expression.ts'
|
|
8
|
+
import {parameterDefaultFits, parameterDefaultLiteral, type ParameterDefaultLiteral} from './literals.ts'
|
|
9
|
+
import {declaredKind, lowerModuleInitializer, scanModuleBindings, tupleHasOptionalOrRestPositions, type ModuleScan} from './module.ts'
|
|
10
|
+
import {scanStaticAnnotations, type StaticAnnotation} from './static-intrinsics.ts'
|
|
11
|
+
import {lowerStatements} from './statements.ts'
|
|
12
|
+
|
|
13
|
+
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
|
+
}
|
|
19
|
+
const staticScan = scanStaticAnnotations(sourceFile, declarations, checker)
|
|
20
|
+
const recordStaticAnnotationIssues = (sites: SourceSpan[]): ProgramIR['staticAnnotationIssues'] =>
|
|
21
|
+
staticScan.outsideTopLevelFunctions.map(call => {
|
|
22
|
+
sites.push(nodeSpan(sourceFile, call))
|
|
23
|
+
return {kind: 'outsideTopLevelFunction', site: sites.length - 1}
|
|
24
|
+
})
|
|
25
|
+
// The two file-wide rejections. An eval string can rewrite bindings that every
|
|
26
|
+
// function's report depends on, and a type-check suppression comment voids the checker's
|
|
27
|
+
// word that every guarantee is built on — in both cases, no function in the file is
|
|
28
|
+
// analyzed.
|
|
29
|
+
const rejectFile = (span: SourceSpan, reason: UnsupportedReason): ProgramIR => {
|
|
30
|
+
const sites = [span]
|
|
31
|
+
const staticAnnotationIssues = recordStaticAnnotationIssues(sites)
|
|
32
|
+
return {
|
|
33
|
+
file: sourceFile.fileName,
|
|
34
|
+
baseDirectory,
|
|
35
|
+
lineStarts: [...sourceFile.getLineStarts()],
|
|
36
|
+
sites,
|
|
37
|
+
functions: declarations.map((declaration, index) => ({
|
|
38
|
+
kind: 'unsupported',
|
|
39
|
+
name: declaration.name!.text,
|
|
40
|
+
hasStaticAnnotations: staticScan.functions[index]!.length > 0,
|
|
41
|
+
site: 0,
|
|
42
|
+
reason,
|
|
43
|
+
})),
|
|
44
|
+
staticAnnotationIssues,
|
|
45
|
+
moduleBindings: [],
|
|
46
|
+
initializer: {
|
|
47
|
+
kind: 'lowered',
|
|
48
|
+
name: moduleInitializerName,
|
|
49
|
+
assertions: [],
|
|
50
|
+
parameters: [],
|
|
51
|
+
returnPropertyNames: null,
|
|
52
|
+
entry: 0,
|
|
53
|
+
blocks: [{loopHeader: null, parameters: [], instructions: [], terminator: {kind: 'stop', site: 0, reason}}],
|
|
54
|
+
},
|
|
55
|
+
initializerSkips: [],
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const suppression = typeCheckSuppressionMention(sourceFile)
|
|
59
|
+
if (suppression != null) return rejectFile(suppression, {kind: 'typeCheckSuppressed'})
|
|
60
|
+
const evalNode = evalMention(sourceFile)
|
|
61
|
+
if (evalNode != null) {
|
|
62
|
+
return rejectFile(nodeSpan(sourceFile, evalNode), {kind: 'evalInFile'})
|
|
63
|
+
}
|
|
64
|
+
const functionsBySymbol = new Map<ts.Symbol, TopLevelFunction>()
|
|
65
|
+
for (let index = 0; index < declarations.length; index++) {
|
|
66
|
+
const declaration = declarations[index]!
|
|
67
|
+
// This loop runs outside the per-function catch below, so a missing symbol here is an
|
|
68
|
+
// invariant crash, not a recorded reason: a declaration name that type-checked always
|
|
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})
|
|
73
|
+
}
|
|
74
|
+
const scan = scanModuleBindings(sourceFile, checker)
|
|
75
|
+
const sites: SourceSpan[] = []
|
|
76
|
+
const staticAnnotationIssues = recordStaticAnnotationIssues(sites)
|
|
77
|
+
const functions: FunctionLowering[] = []
|
|
78
|
+
for (let index = 0; index < declarations.length; index++) {
|
|
79
|
+
const declaration = declarations[index]!
|
|
80
|
+
const staticAnnotations = staticScan.functions[index]!
|
|
81
|
+
// A failed function lowering discards the half-built FunctionContext wholesale; only
|
|
82
|
+
// the name, annotation presence, offending node's site, and tagged reason survive.
|
|
83
|
+
try {
|
|
84
|
+
functions.push(lowerFunction(declaration, staticAnnotations, sourceFile, checker, functionsBySymbol, scan, sites))
|
|
85
|
+
} catch (error) {
|
|
86
|
+
if (!(error instanceof LoweringStop)) throw error
|
|
87
|
+
sites.push(nodeSpan(sourceFile, error.node))
|
|
88
|
+
functions.push({
|
|
89
|
+
kind: 'unsupported',
|
|
90
|
+
name: declaration.name!.text,
|
|
91
|
+
hasStaticAnnotations: staticAnnotations.length > 0,
|
|
92
|
+
site: sites.length - 1,
|
|
93
|
+
reason: error.reason,
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const {initializer, skips} = lowerModuleInitializer(sourceFile, checker, functionsBySymbol, scan, sites)
|
|
98
|
+
return {
|
|
99
|
+
file: sourceFile.fileName,
|
|
100
|
+
baseDirectory,
|
|
101
|
+
lineStarts: [...sourceFile.getLineStarts()],
|
|
102
|
+
sites,
|
|
103
|
+
functions,
|
|
104
|
+
staticAnnotationIssues,
|
|
105
|
+
moduleBindings: scan.bindings,
|
|
106
|
+
initializer,
|
|
107
|
+
initializerSkips: skips,
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function lowerFunction(
|
|
112
|
+
declaration: ts.FunctionDeclaration,
|
|
113
|
+
staticAnnotations: StaticAnnotation[],
|
|
114
|
+
sourceFile: ts.SourceFile,
|
|
115
|
+
checker: ts.TypeChecker,
|
|
116
|
+
functionsBySymbol: Map<ts.Symbol, TopLevelFunction>,
|
|
117
|
+
scan: ModuleScan,
|
|
118
|
+
sites: SourceSpan[],
|
|
119
|
+
): FunctionIR {
|
|
120
|
+
for (const annotation of staticAnnotations) {
|
|
121
|
+
if (annotation.kind === 'invalid') {
|
|
122
|
+
throw unsupported(annotation.node, {kind: 'staticAssertionForm', problem: annotation.problem})
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (declaration.body == null) throw unsupported(declaration, {kind: 'functionWithoutBody'})
|
|
126
|
+
// An async body returns a Promise and a generator returns an iterator; lowering either
|
|
127
|
+
// as if it ran synchronously would publish the body's values as the caller-visible
|
|
128
|
+
// result. Rejected wholesale.
|
|
129
|
+
if (declaration.asteriskToken != null || declaration.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword) === true) {
|
|
130
|
+
throw unsupported(declaration, {kind: 'asyncOrGeneratorFunction'})
|
|
131
|
+
}
|
|
132
|
+
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'})
|
|
139
|
+
}
|
|
140
|
+
const returnType = functionReturnType(declaration, checker)
|
|
141
|
+
// `never` counts as returning nothing: the idiomatic annotation for an always-throwing
|
|
142
|
+
// helper (`function fail(code: number): never`), whose paths all end in throw — the
|
|
143
|
+
// always-throws analysis and the calleeAlwaysThrows caller stop handle the rest.
|
|
144
|
+
const returnsVoid = (returnType.flags & (ts.TypeFlags.Void | ts.TypeFlags.Undefined | ts.TypeFlags.Never)) !== 0
|
|
145
|
+
// Mixed return kinds (e.g. one branch returning a number and another a boolean) would
|
|
146
|
+
// otherwise meet at the engine's return join instead of stopping here.
|
|
147
|
+
if (!returnsVoid && valueKind(returnType, checker) == null) {
|
|
148
|
+
throw unsupported(declaration.type ?? declaration, {kind: 'valueType', typeText: checker.typeToString(returnType)})
|
|
149
|
+
}
|
|
150
|
+
const context = createFunctionContext(
|
|
151
|
+
sourceFile,
|
|
152
|
+
checker,
|
|
153
|
+
functionsBySymbol,
|
|
154
|
+
scan.bindingsBySymbol,
|
|
155
|
+
sites,
|
|
156
|
+
staticAnnotations,
|
|
157
|
+
)
|
|
158
|
+
const entry = context.currentBlock
|
|
159
|
+
for (const parameter of declaration.parameters) {
|
|
160
|
+
// `function area({width, height}: Size)` lowers as a synthetic record parameter plus
|
|
161
|
+
// one property read per name — the same classification named parameters use, the
|
|
162
|
+
// same reads body destructuring uses. The report metadata below keeps the local
|
|
163
|
+
// names, so a condition says `width` rather than `{width, height}.width`. Defaults
|
|
164
|
+
// and rest inside the pattern stay out, like the body form.
|
|
165
|
+
if (ts.isObjectBindingPattern(parameter.name)) {
|
|
166
|
+
const type = lowerParameterType(parameter, checker)
|
|
167
|
+
// The pattern text becomes the parameter's report name; a pattern the author wrapped
|
|
168
|
+
// across source lines would otherwise break the one-fact-per-line report format
|
|
169
|
+
// (`assumes: {` and orphan fragments — a corpus census caught eight of these).
|
|
170
|
+
const patternName = parameter.name.getText(sourceFile).replace(/\s+/g, ' ')
|
|
171
|
+
if (parameter.initializer != null) {
|
|
172
|
+
throw unsupported(parameter, {kind: 'parameterDefaultValue', name: patternName})
|
|
173
|
+
}
|
|
174
|
+
const value = context.nextValue++
|
|
175
|
+
const bindings: Array<{property: string; local: string}> = []
|
|
176
|
+
context.parameters.push({value, name: patternName, type, site: addSite(context, parameter), bindings})
|
|
177
|
+
for (const element of parameter.name.elements) {
|
|
178
|
+
if (!ts.isIdentifier(element.name) || element.dotDotDotToken != null || element.initializer != null) {
|
|
179
|
+
throw unsupported(element, {kind: 'destructuredParameter'})
|
|
180
|
+
}
|
|
181
|
+
const property = element.propertyName == null
|
|
182
|
+
? element.name.text
|
|
183
|
+
: ts.isIdentifier(element.propertyName) ? element.propertyName.text : null
|
|
184
|
+
if (property == null) throw unsupported(element, {kind: 'destructuredParameter'})
|
|
185
|
+
bindings.push({property, local: element.name.text})
|
|
186
|
+
const read: MutableBlock['instructions'][number] = {
|
|
187
|
+
kind: 'property',
|
|
188
|
+
object: value,
|
|
189
|
+
property,
|
|
190
|
+
result: context.nextValue++,
|
|
191
|
+
site: addSite(context, element),
|
|
192
|
+
}
|
|
193
|
+
entry.instructions.push(read)
|
|
194
|
+
context.bindings.set(requiredSymbol(element.name, checker), read.result)
|
|
195
|
+
}
|
|
196
|
+
continue
|
|
197
|
+
}
|
|
198
|
+
if (!ts.isIdentifier(parameter.name)) throw unsupported(parameter.name, {kind: 'destructuredParameter'})
|
|
199
|
+
// A rest parameter is one declaration for any number of arguments; the engine's
|
|
200
|
+
// one-value-per-parameter seeding cannot represent that.
|
|
201
|
+
if (parameter.dotDotDotToken != null) {
|
|
202
|
+
throw unsupported(parameter, {kind: 'parameterType', typeText: `...${checker.typeToString(checker.getTypeAtLocation(parameter))}`, optionalOrRestTuple: false})
|
|
203
|
+
}
|
|
204
|
+
let type = lowerParameterType(parameter, checker)
|
|
205
|
+
// A default value applies whenever a caller omits the argument. Literal defaults can
|
|
206
|
+
// be represented exactly and checked against the declared assumptions: `zoom: number
|
|
207
|
+
// = 5` supplies a finite number. Anything else — `= Infinity`, `= readConfig()` —
|
|
208
|
+
// rejects because it could falsify those assumptions or hide unsupported behavior.
|
|
209
|
+
if (parameter.initializer != null) {
|
|
210
|
+
const default_ = parameterDefaultLiteral(parameter.initializer, checker)
|
|
211
|
+
if (default_ == null || !parameterDefaultFits(default_, type)) {
|
|
212
|
+
throw unsupported(parameter, {kind: 'parameterDefaultValue', name: parameter.name.text})
|
|
213
|
+
}
|
|
214
|
+
type = parameterBodyKind(type, default_)
|
|
215
|
+
}
|
|
216
|
+
const value = context.nextValue++
|
|
217
|
+
context.bindings.set(requiredSymbol(parameter.name, checker), value)
|
|
218
|
+
context.parameters.push({value, name: parameter.name.text, type, site: addSite(context, parameter), bindings: null})
|
|
219
|
+
}
|
|
220
|
+
lowerFiniteInputRequirements(context)
|
|
221
|
+
lowerStatements(declaration.body.statements, context)
|
|
222
|
+
if (context.currentBlock.terminator == null) {
|
|
223
|
+
if (!returnsVoid) {
|
|
224
|
+
// A non-void path reaching the end without a return is a per-path STOP, not a
|
|
225
|
+
// whole-function rejection: an exhaustive switch (or if-chain) over a tagged
|
|
226
|
+
// union's variants makes the fall-out edge provably unreachable — the engine's tag
|
|
227
|
+
// narrowing prunes it and the function analyzes clean, matching how TypeScript's
|
|
228
|
+
// exhaustiveness accepts the same shape under noImplicitReturns. A genuinely
|
|
229
|
+
// reachable fall-out reports as a stop with the returning paths' evidence kept.
|
|
230
|
+
terminate(context.currentBlock, {kind: 'stop', site: addSite(context, declaration), reason: {kind: 'missingReturn'}})
|
|
231
|
+
} else {
|
|
232
|
+
terminate(context.currentBlock, {kind: 'return', value: null, site: addSite(context, declaration)})
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return {
|
|
236
|
+
kind: 'lowered',
|
|
237
|
+
name: declaration.name!.text,
|
|
238
|
+
assertions: context.assertions,
|
|
239
|
+
parameters: context.parameters,
|
|
240
|
+
returnPropertyNames: declaredRecordReturnNames(returnType, checker),
|
|
241
|
+
entry: 0,
|
|
242
|
+
blocks: sealBlocks(context.blocks, declaration.name!.text),
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function lowerFiniteInputRequirements(context: ReturnType<typeof createFunctionContext>): void {
|
|
247
|
+
for (const parameter of context.parameters) {
|
|
248
|
+
for (const properties of finiteInputPaths(parameter.type)) {
|
|
249
|
+
let value = parameter.value
|
|
250
|
+
for (const property of properties) {
|
|
251
|
+
value = addInstructionAtSite(context, parameter.site, {kind: 'property', object: value, property})
|
|
252
|
+
}
|
|
253
|
+
const check = addInstructionAtSite(context, parameter.site, {
|
|
254
|
+
kind: 'numberCheck',
|
|
255
|
+
predicate: 'finite',
|
|
256
|
+
value,
|
|
257
|
+
purpose: 'finiteInput',
|
|
258
|
+
})
|
|
259
|
+
addInstructionAtSite(context, parameter.site, {kind: 'staticRequire', value: check, purpose: 'finiteInput'})
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// JavaScript replaces undefined before the function body begins. A default of undefined
|
|
265
|
+
// leaves it possible; every other default removes it. Null remains an ordinary argument.
|
|
266
|
+
function parameterBodyKind(declared: DeclaredKind, default_: ParameterDefaultLiteral): DeclaredKind {
|
|
267
|
+
if (declared.kind !== 'nullish' || declared.sentinels === 'null') return declared
|
|
268
|
+
if (default_.kind === 'nullish' && default_.sentinel === 'undefined') return declared
|
|
269
|
+
if (declared.sentinels === 'undefined') return declared.inner
|
|
270
|
+
return {kind: 'nullish', inner: declared.inner, sentinels: 'null'}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function declaredRecordReturnNames(returnType: ts.Type, checker: ts.TypeChecker): string[] | null {
|
|
274
|
+
const kind = valueKind(returnType, checker)
|
|
275
|
+
if (kind === 'object') return checker.getPropertiesOfType(returnType).map(property => property.name)
|
|
276
|
+
if (kind === 'nullable' && returnType.isUnion()) {
|
|
277
|
+
const missing = ts.TypeFlags.Null | ts.TypeFlags.Undefined
|
|
278
|
+
const members = returnType.types.filter(member => (member.flags & missing) === 0)
|
|
279
|
+
if (members.length > 0 && members.every(member => valueKind(member, checker) === 'object')) {
|
|
280
|
+
const names = new Set<string>()
|
|
281
|
+
for (const member of members) {
|
|
282
|
+
for (const property of checker.getPropertiesOfType(member)) names.add(property.name)
|
|
283
|
+
}
|
|
284
|
+
return [...names]
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return null
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function lowerParameterType(parameter: ts.ParameterDeclaration, checker: ts.TypeChecker): DeclaredKind {
|
|
291
|
+
// The same recursive classification module bindings use: numbers, booleans, records
|
|
292
|
+
// (opaque leaves included — an id: string property is carried, not rejected), nullable
|
|
293
|
+
// wrappers, arrays, tuples, and bare opaque (a plain string parameter).
|
|
294
|
+
const type = checker.getTypeAtLocation(parameter)
|
|
295
|
+
const declared = declaredKind(type, checker, [])
|
|
296
|
+
if (declared == null) {
|
|
297
|
+
throw unsupported(parameter, {
|
|
298
|
+
kind: 'parameterType',
|
|
299
|
+
typeText: checker.typeToString(type),
|
|
300
|
+
optionalOrRestTuple: tupleHasOptionalOrRestPositions(type, checker),
|
|
301
|
+
})
|
|
302
|
+
}
|
|
303
|
+
return declared
|
|
304
|
+
}
|
|
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
|
+
}
|