@tsonic/frontend 0.0.12 → 0.0.14
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/dist/.tsbuildinfo +1 -1
- package/dist/ir/converters/expressions/access.d.ts.map +1 -1
- package/dist/ir/converters/expressions/access.js +61 -1
- package/dist/ir/converters/expressions/access.js.map +1 -1
- package/dist/ir/converters/expressions/calls.d.ts +2 -2
- package/dist/ir/converters/expressions/calls.d.ts.map +1 -1
- package/dist/ir/converters/expressions/calls.js +318 -24
- package/dist/ir/converters/expressions/calls.js.map +1 -1
- package/dist/ir/converters/expressions/helpers.js +4 -4
- package/dist/ir/converters/expressions/helpers.js.map +1 -1
- package/dist/ir/converters/expressions/literals.d.ts +14 -0
- package/dist/ir/converters/expressions/literals.d.ts.map +1 -1
- package/dist/ir/converters/expressions/literals.js +22 -2
- package/dist/ir/converters/expressions/literals.js.map +1 -1
- package/dist/ir/converters/expressions/numeric-recovery.test.js +3 -2
- package/dist/ir/converters/expressions/numeric-recovery.test.js.map +1 -1
- package/dist/ir/converters/statements/helpers.d.ts.map +1 -1
- package/dist/ir/converters/statements/helpers.js +10 -4
- package/dist/ir/converters/statements/helpers.js.map +1 -1
- package/dist/ir/expression-converter.d.ts.map +1 -1
- package/dist/ir/expression-converter.js +59 -7
- package/dist/ir/expression-converter.js.map +1 -1
- package/dist/ir/index.d.ts +1 -1
- package/dist/ir/index.d.ts.map +1 -1
- package/dist/ir/index.js +1 -1
- package/dist/ir/index.js.map +1 -1
- package/dist/ir/statement-converter.d.ts.map +1 -1
- package/dist/ir/statement-converter.js +12 -0
- package/dist/ir/statement-converter.js.map +1 -1
- package/dist/ir/type-converter/inference.d.ts.map +1 -1
- package/dist/ir/type-converter/inference.js +50 -7
- package/dist/ir/type-converter/inference.js.map +1 -1
- package/dist/ir/type-converter/primitives.d.ts +26 -4
- package/dist/ir/type-converter/primitives.d.ts.map +1 -1
- package/dist/ir/type-converter/primitives.js +36 -2
- package/dist/ir/type-converter/primitives.js.map +1 -1
- package/dist/ir/type-converter/references.d.ts.map +1 -1
- package/dist/ir/type-converter/references.js +324 -11
- package/dist/ir/type-converter/references.js.map +1 -1
- package/dist/ir/type-converter/utility-types.d.ts +93 -0
- package/dist/ir/type-converter/utility-types.d.ts.map +1 -0
- package/dist/ir/type-converter/utility-types.js +528 -0
- package/dist/ir/type-converter/utility-types.js.map +1 -0
- package/dist/ir/type-converter/utility-types.test.d.ts +10 -0
- package/dist/ir/type-converter/utility-types.test.d.ts.map +1 -0
- package/dist/ir/type-converter/utility-types.test.js +1030 -0
- package/dist/ir/type-converter/utility-types.test.js.map +1 -0
- package/dist/ir/types/expressions.d.ts +87 -3
- package/dist/ir/types/expressions.d.ts.map +1 -1
- package/dist/ir/types/helpers.d.ts +3 -1
- package/dist/ir/types/helpers.d.ts.map +1 -1
- package/dist/ir/types/index.d.ts +3 -2
- package/dist/ir/types/index.d.ts.map +1 -1
- package/dist/ir/types/index.js +2 -0
- package/dist/ir/types/index.js.map +1 -1
- package/dist/ir/types/ir-types.d.ts +69 -11
- package/dist/ir/types/ir-types.d.ts.map +1 -1
- package/dist/ir/types/numeric-helpers.d.ts +46 -0
- package/dist/ir/types/numeric-helpers.d.ts.map +1 -0
- package/dist/ir/types/numeric-helpers.js +105 -0
- package/dist/ir/types/numeric-helpers.js.map +1 -0
- package/dist/ir/types/statements.d.ts +11 -1
- package/dist/ir/types/statements.d.ts.map +1 -1
- package/dist/ir/types.d.ts +2 -2
- package/dist/ir/types.d.ts.map +1 -1
- package/dist/ir/types.js +3 -1
- package/dist/ir/types.js.map +1 -1
- package/dist/ir/validation/anonymous-type-lowering-pass.d.ts +32 -0
- package/dist/ir/validation/anonymous-type-lowering-pass.d.ts.map +1 -0
- package/dist/ir/validation/anonymous-type-lowering-pass.js +868 -0
- package/dist/ir/validation/anonymous-type-lowering-pass.js.map +1 -0
- package/dist/ir/validation/attribute-collection-pass.d.ts +37 -0
- package/dist/ir/validation/attribute-collection-pass.d.ts.map +1 -0
- package/dist/ir/validation/attribute-collection-pass.js +282 -0
- package/dist/ir/validation/attribute-collection-pass.js.map +1 -0
- package/dist/ir/validation/attribute-collection-pass.test.d.ts +5 -0
- package/dist/ir/validation/attribute-collection-pass.test.d.ts.map +1 -0
- package/dist/ir/validation/attribute-collection-pass.test.js +215 -0
- package/dist/ir/validation/attribute-collection-pass.test.js.map +1 -0
- package/dist/ir/validation/index.d.ts +3 -0
- package/dist/ir/validation/index.d.ts.map +1 -1
- package/dist/ir/validation/index.js +3 -0
- package/dist/ir/validation/index.js.map +1 -1
- package/dist/ir/validation/numeric-coercion-pass.d.ts +77 -0
- package/dist/ir/validation/numeric-coercion-pass.d.ts.map +1 -0
- package/dist/ir/validation/numeric-coercion-pass.js +686 -0
- package/dist/ir/validation/numeric-coercion-pass.js.map +1 -0
- package/dist/ir/validation/numeric-invariants.test.js +130 -14
- package/dist/ir/validation/numeric-invariants.test.js.map +1 -1
- package/dist/ir/validation/numeric-proof-pass.d.ts.map +1 -1
- package/dist/ir/validation/numeric-proof-pass.js +68 -108
- package/dist/ir/validation/numeric-proof-pass.js.map +1 -1
- package/dist/ir/validation/soundness-gate.d.ts.map +1 -1
- package/dist/ir/validation/soundness-gate.js +23 -12
- package/dist/ir/validation/soundness-gate.js.map +1 -1
- package/dist/ir/validation/yield-lowering-pass.test.js +21 -12
- package/dist/ir/validation/yield-lowering-pass.test.js.map +1 -1
- package/dist/program/bindings.d.ts +20 -9
- package/dist/program/bindings.d.ts.map +1 -1
- package/dist/program/bindings.js +98 -36
- package/dist/program/bindings.js.map +1 -1
- package/dist/program/bindings.test.js +3 -3
- package/dist/program/dependency-graph.d.ts.map +1 -1
- package/dist/program/dependency-graph.js +31 -5
- package/dist/program/dependency-graph.js.map +1 -1
- package/dist/resolver/import-resolution.d.ts +4 -0
- package/dist/resolver/import-resolution.d.ts.map +1 -1
- package/dist/resolver/import-resolution.js +18 -7
- package/dist/resolver/import-resolution.js.map +1 -1
- package/dist/resolver.test.js +2 -2
- package/dist/resolver.test.js.map +1 -1
- package/dist/types/diagnostic.d.ts +1 -1
- package/dist/types/diagnostic.d.ts.map +1 -1
- package/dist/types/diagnostic.js.map +1 -1
- package/dist/validation/generics.d.ts.map +1 -1
- package/dist/validation/generics.js +133 -1
- package/dist/validation/generics.js.map +1 -1
- package/dist/validation/static-safety.js +13 -0
- package/dist/validation/static-safety.js.map +1 -1
- package/dist/validation/unsupported-utility-types.d.ts +10 -8
- package/dist/validation/unsupported-utility-types.d.ts.map +1 -1
- package/dist/validation/unsupported-utility-types.js +12 -19
- package/dist/validation/unsupported-utility-types.js.map +1 -1
- package/dist/validator.test.js +133 -28
- package/dist/validator.test.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,686 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Numeric Coercion Pass - STRICT CONTRACT enforcement
|
|
3
|
+
*
|
|
4
|
+
* This pass detects cases where an integer expression is used where a double is expected,
|
|
5
|
+
* and requires explicit user intent for the conversion.
|
|
6
|
+
*
|
|
7
|
+
* STRICT RULE: int → double requires explicit user intent
|
|
8
|
+
*
|
|
9
|
+
* Intent sites (where widening is checked):
|
|
10
|
+
* 1. Variable initialization with explicit type: `const x: number = 42` → ERROR
|
|
11
|
+
* 2. Parameter passing: `foo(42)` where foo expects `number` → ERROR
|
|
12
|
+
* 3. Return statements: `return 42` where function returns `number` → ERROR
|
|
13
|
+
* 4. Array elements: `[1, 2, 3]` in `number[]` context → ERROR (each element)
|
|
14
|
+
* 5. Object properties: `{ x: 42 }` where type has `x: number` → ERROR
|
|
15
|
+
* 6. Ternary branches: `cond ? 1 : 2` where expected is `number` → ERROR
|
|
16
|
+
* 7. Default parameters: `function f(x: number = 42)` → ERROR
|
|
17
|
+
* 8. Tuple elements: `[1, 2]` in `[number, number]` context → ERROR
|
|
18
|
+
*
|
|
19
|
+
* How to satisfy the contract:
|
|
20
|
+
* - Use double literal: `const x: number = 42.0` ✓
|
|
21
|
+
* - Use explicit cast: `const x: number = 42 as number` ✓
|
|
22
|
+
*
|
|
23
|
+
* This pass runs AFTER the IR is built, BEFORE emission.
|
|
24
|
+
* It is a HARD GATE - any errors prevent emission.
|
|
25
|
+
*/
|
|
26
|
+
import { createDiagnostic, } from "../../types/diagnostic.js";
|
|
27
|
+
import { getBinaryResultKind, } from "../types.js";
|
|
28
|
+
/**
|
|
29
|
+
* Arithmetic operators that produce numeric results.
|
|
30
|
+
* Used to classify binary expression results.
|
|
31
|
+
*/
|
|
32
|
+
const ARITHMETIC_OPERATORS = new Set(["+", "-", "*", "/", "%"]);
|
|
33
|
+
/**
|
|
34
|
+
* Classify an expression's numeric kind.
|
|
35
|
+
*
|
|
36
|
+
* This function propagates numeric kind through:
|
|
37
|
+
* - Literals (via numericIntent)
|
|
38
|
+
* - Identifiers with primitiveType(name="int") or primitiveType(name="number")
|
|
39
|
+
* - Arithmetic operations (uses C# promotion rules)
|
|
40
|
+
* - Unary +/- (preserves operand kind)
|
|
41
|
+
* - Ternary (requires both branches to have same kind)
|
|
42
|
+
* - Parentheses (pass through)
|
|
43
|
+
* - numericNarrowing expressions (uses targetKind)
|
|
44
|
+
*
|
|
45
|
+
* Returns "Unknown" for expressions that cannot be classified.
|
|
46
|
+
*/
|
|
47
|
+
export const classifyNumericExpr = (expr) => {
|
|
48
|
+
switch (expr.kind) {
|
|
49
|
+
case "literal": {
|
|
50
|
+
// Check numericIntent on literal expressions
|
|
51
|
+
if (typeof expr.value === "number" && expr.numericIntent) {
|
|
52
|
+
return expr.numericIntent === "Int32" ? "Int32" : "Double";
|
|
53
|
+
}
|
|
54
|
+
// Non-numeric literals
|
|
55
|
+
return "Unknown";
|
|
56
|
+
}
|
|
57
|
+
case "identifier": {
|
|
58
|
+
// Check inferredType on identifiers
|
|
59
|
+
if (expr.inferredType?.kind === "primitiveType") {
|
|
60
|
+
if (expr.inferredType.name === "int")
|
|
61
|
+
return "Int32";
|
|
62
|
+
if (expr.inferredType.name === "number")
|
|
63
|
+
return "Double";
|
|
64
|
+
}
|
|
65
|
+
// Also check for CLR numeric reference types
|
|
66
|
+
if (expr.inferredType?.kind === "referenceType") {
|
|
67
|
+
const name = expr.inferredType.name;
|
|
68
|
+
if (name === "Int32" || name === "int")
|
|
69
|
+
return "Int32";
|
|
70
|
+
if (name === "Double" || name === "double")
|
|
71
|
+
return "Double";
|
|
72
|
+
}
|
|
73
|
+
return "Unknown";
|
|
74
|
+
}
|
|
75
|
+
case "unary": {
|
|
76
|
+
// Unary +/- preserves operand kind
|
|
77
|
+
if (expr.operator === "+" || expr.operator === "-") {
|
|
78
|
+
return classifyNumericExpr(expr.expression);
|
|
79
|
+
}
|
|
80
|
+
// Bitwise NOT (~) produces Int32
|
|
81
|
+
if (expr.operator === "~") {
|
|
82
|
+
return "Int32";
|
|
83
|
+
}
|
|
84
|
+
return "Unknown";
|
|
85
|
+
}
|
|
86
|
+
case "binary": {
|
|
87
|
+
// Only classify arithmetic operators
|
|
88
|
+
if (!ARITHMETIC_OPERATORS.has(expr.operator)) {
|
|
89
|
+
return "Unknown";
|
|
90
|
+
}
|
|
91
|
+
const leftKind = classifyNumericExpr(expr.left);
|
|
92
|
+
const rightKind = classifyNumericExpr(expr.right);
|
|
93
|
+
// If either is Unknown, we can't classify
|
|
94
|
+
if (leftKind === "Unknown" || rightKind === "Unknown") {
|
|
95
|
+
return "Unknown";
|
|
96
|
+
}
|
|
97
|
+
// Use C# binary promotion rules
|
|
98
|
+
// Note: getBinaryResultKind returns NumericKind, we map to our simplified type
|
|
99
|
+
const resultKind = getBinaryResultKind(leftKind === "Int32" ? "Int32" : "Double", rightKind === "Int32" ? "Int32" : "Double");
|
|
100
|
+
// Map back to our simplified kind
|
|
101
|
+
if (resultKind === "Double" || resultKind === "Single")
|
|
102
|
+
return "Double";
|
|
103
|
+
return "Int32"; // All integer promotions end up as at least Int32
|
|
104
|
+
}
|
|
105
|
+
case "conditional": {
|
|
106
|
+
// Ternary: both branches must have same kind
|
|
107
|
+
const trueKind = classifyNumericExpr(expr.whenTrue);
|
|
108
|
+
const falseKind = classifyNumericExpr(expr.whenFalse);
|
|
109
|
+
if (trueKind === falseKind)
|
|
110
|
+
return trueKind;
|
|
111
|
+
// Mismatched branches - use promotion
|
|
112
|
+
if (trueKind === "Double" || falseKind === "Double")
|
|
113
|
+
return "Double";
|
|
114
|
+
return "Unknown";
|
|
115
|
+
}
|
|
116
|
+
case "numericNarrowing": {
|
|
117
|
+
// numericNarrowing has explicit targetKind
|
|
118
|
+
if (expr.targetKind === "Int32")
|
|
119
|
+
return "Int32";
|
|
120
|
+
if (expr.targetKind === "Double")
|
|
121
|
+
return "Double";
|
|
122
|
+
// Other numeric kinds (Byte, Int64, etc.) - treat as Unknown for this pass
|
|
123
|
+
return "Unknown";
|
|
124
|
+
}
|
|
125
|
+
case "call": {
|
|
126
|
+
// Check return type
|
|
127
|
+
if (expr.inferredType?.kind === "primitiveType") {
|
|
128
|
+
if (expr.inferredType.name === "int")
|
|
129
|
+
return "Int32";
|
|
130
|
+
if (expr.inferredType.name === "number")
|
|
131
|
+
return "Double";
|
|
132
|
+
}
|
|
133
|
+
return "Unknown";
|
|
134
|
+
}
|
|
135
|
+
case "memberAccess": {
|
|
136
|
+
// Check inferredType for member access results
|
|
137
|
+
if (expr.inferredType?.kind === "primitiveType") {
|
|
138
|
+
if (expr.inferredType.name === "int")
|
|
139
|
+
return "Int32";
|
|
140
|
+
if (expr.inferredType.name === "number")
|
|
141
|
+
return "Double";
|
|
142
|
+
}
|
|
143
|
+
return "Unknown";
|
|
144
|
+
}
|
|
145
|
+
case "update": {
|
|
146
|
+
// ++/-- on int produces int
|
|
147
|
+
const operandKind = classifyNumericExpr(expr.expression);
|
|
148
|
+
return operandKind;
|
|
149
|
+
}
|
|
150
|
+
default:
|
|
151
|
+
return "Unknown";
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
/**
|
|
155
|
+
* Check if an expression has explicit double intent.
|
|
156
|
+
* This is true when:
|
|
157
|
+
* - It's a numericNarrowing with targetKind "Double" (i.e., `42 as number`)
|
|
158
|
+
* - It's a literal with numericIntent "Double" (i.e., `42.0`)
|
|
159
|
+
*
|
|
160
|
+
* Used to exempt explicit casts from TSN5110 errors.
|
|
161
|
+
*/
|
|
162
|
+
export const hasExplicitDoubleIntent = (expr) => {
|
|
163
|
+
// Case 1: numericNarrowing targeting Double (e.g., `42 as number`, `42 as double`)
|
|
164
|
+
if (expr.kind === "numericNarrowing" && expr.targetKind === "Double") {
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
// Case 2: Literal with double lexeme (e.g., `42.0`)
|
|
168
|
+
if (expr.kind === "literal" &&
|
|
169
|
+
typeof expr.value === "number" &&
|
|
170
|
+
expr.numericIntent === "Double") {
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
return false;
|
|
174
|
+
};
|
|
175
|
+
/**
|
|
176
|
+
* Create a source location for a module
|
|
177
|
+
*/
|
|
178
|
+
const moduleLocation = (ctx) => ({
|
|
179
|
+
file: ctx.filePath,
|
|
180
|
+
line: 1,
|
|
181
|
+
column: 1,
|
|
182
|
+
length: 1,
|
|
183
|
+
});
|
|
184
|
+
/**
|
|
185
|
+
* Check if a type is "number" (which ALWAYS means double)
|
|
186
|
+
*
|
|
187
|
+
* INVARIANT A: "number" always means C# "double". No exceptions.
|
|
188
|
+
* INVARIANT B: "int" is a distinct primitive type, NOT number with numericIntent.
|
|
189
|
+
*/
|
|
190
|
+
const isNumberType = (type) => {
|
|
191
|
+
if (!type)
|
|
192
|
+
return false;
|
|
193
|
+
// primitiveType(name="number") is ALWAYS double
|
|
194
|
+
return type.kind === "primitiveType" && type.name === "number";
|
|
195
|
+
};
|
|
196
|
+
/**
|
|
197
|
+
* Extract the expected type of a property from a structural type.
|
|
198
|
+
*
|
|
199
|
+
* Used for validating object literal properties against their expected types.
|
|
200
|
+
* Returns undefined if the property type cannot be determined (conservative).
|
|
201
|
+
*
|
|
202
|
+
* Handles:
|
|
203
|
+
* - objectType: inline object types like `{ x: number }`
|
|
204
|
+
* - referenceType with structuralMembers: interfaces and type aliases
|
|
205
|
+
*/
|
|
206
|
+
const tryGetObjectPropertyType = (expectedType, propName) => {
|
|
207
|
+
if (!expectedType)
|
|
208
|
+
return undefined;
|
|
209
|
+
// Structural object type: objectType has members directly
|
|
210
|
+
if (expectedType.kind === "objectType") {
|
|
211
|
+
const member = expectedType.members.find((m) => m.kind === "propertySignature" && m.name === propName);
|
|
212
|
+
return member?.type;
|
|
213
|
+
}
|
|
214
|
+
// Reference type with structural members (interfaces, type aliases)
|
|
215
|
+
if (expectedType.kind === "referenceType" && expectedType.structuralMembers) {
|
|
216
|
+
const member = expectedType.structuralMembers.find((m) => m.kind === "propertySignature" && m.name === propName);
|
|
217
|
+
return member?.type;
|
|
218
|
+
}
|
|
219
|
+
return undefined;
|
|
220
|
+
};
|
|
221
|
+
/**
|
|
222
|
+
* Extract the expected type of a tuple element at a given index.
|
|
223
|
+
*
|
|
224
|
+
* Used for validating tuple literal elements against their expected types.
|
|
225
|
+
* Returns undefined if the element type cannot be determined.
|
|
226
|
+
*/
|
|
227
|
+
const tryGetTupleElementType = (expectedType, index) => {
|
|
228
|
+
if (!expectedType)
|
|
229
|
+
return undefined;
|
|
230
|
+
if (expectedType.kind === "tupleType") {
|
|
231
|
+
return expectedType.elementTypes[index];
|
|
232
|
+
}
|
|
233
|
+
return undefined;
|
|
234
|
+
};
|
|
235
|
+
/**
|
|
236
|
+
* Check if an expression is an integer expression (Int32).
|
|
237
|
+
* Uses the expression classifier to handle composed expressions.
|
|
238
|
+
*/
|
|
239
|
+
const isIntegerExpression = (expr) => {
|
|
240
|
+
return classifyNumericExpr(expr) === "Int32";
|
|
241
|
+
};
|
|
242
|
+
/**
|
|
243
|
+
* Check if an expression needs coercion to match an expected double type.
|
|
244
|
+
* Returns true if:
|
|
245
|
+
* - Expected type is "number" (double)
|
|
246
|
+
* - Expression classifies as Int32
|
|
247
|
+
* - Expression does NOT have explicit double intent (cast exemption)
|
|
248
|
+
*/
|
|
249
|
+
const needsCoercion = (expr, expectedType) => {
|
|
250
|
+
// Only check if expected type is unadorned "number" (meaning double)
|
|
251
|
+
if (!isNumberType(expectedType)) {
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
// Check for explicit double intent (e.g., `42 as number`, `42.0`)
|
|
255
|
+
// These are explicitly allowed even though they classify as Int32
|
|
256
|
+
if (hasExplicitDoubleIntent(expr)) {
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
// Check if the expression classifies as Int32
|
|
260
|
+
return isIntegerExpression(expr);
|
|
261
|
+
};
|
|
262
|
+
/**
|
|
263
|
+
* Get a human-readable description of an expression for error messages.
|
|
264
|
+
*/
|
|
265
|
+
const describeExpression = (expr) => {
|
|
266
|
+
switch (expr.kind) {
|
|
267
|
+
case "literal":
|
|
268
|
+
return `literal '${expr.raw ?? String(expr.value)}'`;
|
|
269
|
+
case "identifier":
|
|
270
|
+
return `identifier '${expr.name}'`;
|
|
271
|
+
case "binary":
|
|
272
|
+
return `arithmetic expression`;
|
|
273
|
+
case "unary":
|
|
274
|
+
return `unary expression`;
|
|
275
|
+
case "conditional":
|
|
276
|
+
return `ternary expression`;
|
|
277
|
+
case "call":
|
|
278
|
+
return `call result`;
|
|
279
|
+
case "memberAccess":
|
|
280
|
+
return typeof expr.property === "string"
|
|
281
|
+
? `property '${expr.property}'`
|
|
282
|
+
: `computed property`;
|
|
283
|
+
default:
|
|
284
|
+
return `expression`;
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
/**
|
|
288
|
+
* Emit an error diagnostic for int→double coercion
|
|
289
|
+
*/
|
|
290
|
+
const emitCoercionError = (expr, ctx, context) => {
|
|
291
|
+
const location = expr.sourceSpan ?? moduleLocation(ctx);
|
|
292
|
+
const description = describeExpression(expr);
|
|
293
|
+
ctx.diagnostics.push(createDiagnostic("TSN5110", "error", `Integer ${description} cannot be implicitly converted to 'number' (double) ${context}`, location, expr.kind === "literal"
|
|
294
|
+
? `Use a double literal (e.g., '${expr.raw ?? expr.value}.0') or explicit cast ('${expr.raw ?? expr.value} as number').`
|
|
295
|
+
: `Use an explicit cast (e.g., 'expr as number') to convert to double.`));
|
|
296
|
+
};
|
|
297
|
+
/**
|
|
298
|
+
* Validate an expression in a context where a specific type is expected.
|
|
299
|
+
* This is the core of the strict coercion check.
|
|
300
|
+
*/
|
|
301
|
+
const validateExpression = (expr, expectedType, ctx, context) => {
|
|
302
|
+
// Check for direct int→double coercion
|
|
303
|
+
if (needsCoercion(expr, expectedType)) {
|
|
304
|
+
emitCoercionError(expr, ctx, context);
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
// Recursively check sub-expressions based on kind
|
|
308
|
+
switch (expr.kind) {
|
|
309
|
+
case "array": {
|
|
310
|
+
// For tuple types, validate each element against its specific expected type
|
|
311
|
+
if (expectedType?.kind === "tupleType") {
|
|
312
|
+
expr.elements.forEach((el, i) => {
|
|
313
|
+
if (el && el.kind !== "spread") {
|
|
314
|
+
const tupleElementType = tryGetTupleElementType(expectedType, i);
|
|
315
|
+
validateExpression(el, tupleElementType, ctx, `in tuple element ${i}`);
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
// For array types, check each element against the element type
|
|
321
|
+
const elementType = expectedType?.kind === "arrayType"
|
|
322
|
+
? expectedType.elementType
|
|
323
|
+
: undefined;
|
|
324
|
+
expr.elements.forEach((el, i) => {
|
|
325
|
+
if (el && el.kind !== "spread") {
|
|
326
|
+
validateExpression(el, elementType, ctx, `in array element ${i}`);
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
break;
|
|
331
|
+
}
|
|
332
|
+
case "object": {
|
|
333
|
+
// For object literals, check each property against expected property type
|
|
334
|
+
// Uses contextual expectedType only - no guessing
|
|
335
|
+
expr.properties.forEach((prop) => {
|
|
336
|
+
if (prop.kind === "spread") {
|
|
337
|
+
// For spreads, scan for nested call expressions
|
|
338
|
+
scanExpressionForCalls(prop.expression, ctx);
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
// Only handle string keys (not computed expressions)
|
|
342
|
+
if (typeof prop.key === "string") {
|
|
343
|
+
// Get expected type for this property from contextual type
|
|
344
|
+
const expectedPropType = tryGetObjectPropertyType(expectedType, prop.key);
|
|
345
|
+
if (expectedPropType) {
|
|
346
|
+
validateExpression(prop.value, expectedPropType, ctx, `in property '${prop.key}'`);
|
|
347
|
+
}
|
|
348
|
+
else {
|
|
349
|
+
// Can't determine property type - scan for nested calls
|
|
350
|
+
scanExpressionForCalls(prop.value, ctx);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
else {
|
|
354
|
+
// Computed property key - can't resolve type, scan for calls
|
|
355
|
+
scanExpressionForCalls(prop.value, ctx);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
break;
|
|
360
|
+
}
|
|
361
|
+
case "conditional": {
|
|
362
|
+
// Check both branches
|
|
363
|
+
validateExpression(expr.whenTrue, expectedType, ctx, context);
|
|
364
|
+
validateExpression(expr.whenFalse, expectedType, ctx, context);
|
|
365
|
+
break;
|
|
366
|
+
}
|
|
367
|
+
case "logical": {
|
|
368
|
+
// For ?? and ||, the result could be either operand
|
|
369
|
+
if (expr.operator === "??" || expr.operator === "||") {
|
|
370
|
+
validateExpression(expr.left, expectedType, ctx, context);
|
|
371
|
+
validateExpression(expr.right, expectedType, ctx, context);
|
|
372
|
+
}
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
375
|
+
case "call": {
|
|
376
|
+
// Check each argument against expected parameter type
|
|
377
|
+
if (expr.parameterTypes) {
|
|
378
|
+
expr.arguments.forEach((arg, i) => {
|
|
379
|
+
if (arg.kind !== "spread" && expr.parameterTypes?.[i]) {
|
|
380
|
+
validateExpression(arg, expr.parameterTypes[i], ctx, `in argument ${i + 1}`);
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
break;
|
|
385
|
+
}
|
|
386
|
+
// Other expression kinds don't need recursive checking for this pass
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
/**
|
|
390
|
+
* Scan an expression tree for call expressions and validate their arguments.
|
|
391
|
+
* This is used for expressions without an explicit type context.
|
|
392
|
+
*/
|
|
393
|
+
const scanExpressionForCalls = (expr, ctx) => {
|
|
394
|
+
switch (expr.kind) {
|
|
395
|
+
case "call": {
|
|
396
|
+
// Validate call arguments against parameter types
|
|
397
|
+
if (expr.parameterTypes) {
|
|
398
|
+
expr.arguments.forEach((arg, i) => {
|
|
399
|
+
if (arg.kind !== "spread" && expr.parameterTypes?.[i]) {
|
|
400
|
+
validateExpression(arg, expr.parameterTypes[i], ctx, `in argument ${i + 1}`);
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
// Also scan the callee for nested calls
|
|
405
|
+
scanExpressionForCalls(expr.callee, ctx);
|
|
406
|
+
// Scan arguments for nested calls
|
|
407
|
+
expr.arguments.forEach((arg) => {
|
|
408
|
+
if (arg.kind !== "spread") {
|
|
409
|
+
scanExpressionForCalls(arg, ctx);
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
break;
|
|
413
|
+
}
|
|
414
|
+
case "array": {
|
|
415
|
+
expr.elements.forEach((el) => {
|
|
416
|
+
if (el && el.kind !== "spread") {
|
|
417
|
+
scanExpressionForCalls(el, ctx);
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
break;
|
|
421
|
+
}
|
|
422
|
+
case "object": {
|
|
423
|
+
expr.properties.forEach((prop) => {
|
|
424
|
+
if (prop.kind !== "spread") {
|
|
425
|
+
scanExpressionForCalls(prop.value, ctx);
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
break;
|
|
429
|
+
}
|
|
430
|
+
case "binary":
|
|
431
|
+
scanExpressionForCalls(expr.left, ctx);
|
|
432
|
+
scanExpressionForCalls(expr.right, ctx);
|
|
433
|
+
break;
|
|
434
|
+
case "unary":
|
|
435
|
+
scanExpressionForCalls(expr.expression, ctx);
|
|
436
|
+
break;
|
|
437
|
+
case "update":
|
|
438
|
+
scanExpressionForCalls(expr.expression, ctx);
|
|
439
|
+
break;
|
|
440
|
+
case "conditional":
|
|
441
|
+
scanExpressionForCalls(expr.condition, ctx);
|
|
442
|
+
scanExpressionForCalls(expr.whenTrue, ctx);
|
|
443
|
+
scanExpressionForCalls(expr.whenFalse, ctx);
|
|
444
|
+
break;
|
|
445
|
+
case "logical":
|
|
446
|
+
scanExpressionForCalls(expr.left, ctx);
|
|
447
|
+
scanExpressionForCalls(expr.right, ctx);
|
|
448
|
+
break;
|
|
449
|
+
case "memberAccess":
|
|
450
|
+
scanExpressionForCalls(expr.object, ctx);
|
|
451
|
+
// For computed access, property is an expression
|
|
452
|
+
if (expr.isComputed && typeof expr.property !== "string") {
|
|
453
|
+
scanExpressionForCalls(expr.property, ctx);
|
|
454
|
+
}
|
|
455
|
+
break;
|
|
456
|
+
case "arrowFunction":
|
|
457
|
+
// Arrow function body can be expression or block
|
|
458
|
+
if ("kind" in expr.body && expr.body.kind !== "blockStatement") {
|
|
459
|
+
scanExpressionForCalls(expr.body, ctx);
|
|
460
|
+
}
|
|
461
|
+
break;
|
|
462
|
+
case "new":
|
|
463
|
+
expr.arguments.forEach((arg) => {
|
|
464
|
+
if (arg.kind !== "spread") {
|
|
465
|
+
scanExpressionForCalls(arg, ctx);
|
|
466
|
+
}
|
|
467
|
+
});
|
|
468
|
+
break;
|
|
469
|
+
case "await":
|
|
470
|
+
scanExpressionForCalls(expr.expression, ctx);
|
|
471
|
+
break;
|
|
472
|
+
case "assignment":
|
|
473
|
+
scanExpressionForCalls(expr.right, ctx);
|
|
474
|
+
break;
|
|
475
|
+
case "numericNarrowing":
|
|
476
|
+
scanExpressionForCalls(expr.expression, ctx);
|
|
477
|
+
break;
|
|
478
|
+
case "yield":
|
|
479
|
+
if (expr.expression) {
|
|
480
|
+
scanExpressionForCalls(expr.expression, ctx);
|
|
481
|
+
}
|
|
482
|
+
break;
|
|
483
|
+
// Leaf expressions: literal, identifier, this - no nested calls
|
|
484
|
+
default:
|
|
485
|
+
break;
|
|
486
|
+
}
|
|
487
|
+
};
|
|
488
|
+
/**
|
|
489
|
+
* Process a statement, checking for int→double coercion at intent sites.
|
|
490
|
+
*/
|
|
491
|
+
const processStatement = (stmt, ctx) => {
|
|
492
|
+
switch (stmt.kind) {
|
|
493
|
+
case "variableDeclaration": {
|
|
494
|
+
for (const decl of stmt.declarations) {
|
|
495
|
+
if (decl.initializer) {
|
|
496
|
+
// Check if there's an explicit type annotation
|
|
497
|
+
if (decl.type) {
|
|
498
|
+
validateExpression(decl.initializer, decl.type, ctx, "in variable initialization");
|
|
499
|
+
}
|
|
500
|
+
else {
|
|
501
|
+
// Even without explicit type, scan for call expressions
|
|
502
|
+
// to check their arguments
|
|
503
|
+
scanExpressionForCalls(decl.initializer, ctx);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
break;
|
|
508
|
+
}
|
|
509
|
+
case "returnStatement": {
|
|
510
|
+
// We'd need function context to know expected return type
|
|
511
|
+
// For now, skip - this requires threading function return type through
|
|
512
|
+
break;
|
|
513
|
+
}
|
|
514
|
+
case "expressionStatement": {
|
|
515
|
+
// Check call expressions for parameter coercion
|
|
516
|
+
if (stmt.expression.kind === "call") {
|
|
517
|
+
const call = stmt.expression;
|
|
518
|
+
// Check each argument against expected parameter type
|
|
519
|
+
if (call.parameterTypes) {
|
|
520
|
+
call.arguments.forEach((arg, i) => {
|
|
521
|
+
if (arg.kind !== "spread" && call.parameterTypes?.[i]) {
|
|
522
|
+
validateExpression(arg, call.parameterTypes[i], ctx, `in argument ${i + 1}`);
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
break;
|
|
528
|
+
}
|
|
529
|
+
case "functionDeclaration": {
|
|
530
|
+
// Check default parameters for int→double coercion
|
|
531
|
+
for (const param of stmt.parameters) {
|
|
532
|
+
if (param.initializer && param.type) {
|
|
533
|
+
validateExpression(param.initializer, param.type, ctx, "in default parameter");
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
// Process function body with return type context
|
|
537
|
+
processStatementWithReturnType(stmt.body, stmt.returnType, ctx);
|
|
538
|
+
break;
|
|
539
|
+
}
|
|
540
|
+
case "classDeclaration": {
|
|
541
|
+
for (const member of stmt.members) {
|
|
542
|
+
if (member.kind === "methodDeclaration") {
|
|
543
|
+
// Check default parameters for int→double coercion
|
|
544
|
+
for (const param of member.parameters) {
|
|
545
|
+
if (param.initializer && param.type) {
|
|
546
|
+
validateExpression(param.initializer, param.type, ctx, "in default parameter");
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
if (member.body) {
|
|
550
|
+
processStatementWithReturnType(member.body, member.returnType, ctx);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
if (member.kind === "propertyDeclaration" && member.initializer) {
|
|
554
|
+
validateExpression(member.initializer, member.type, ctx, "in property initialization");
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
break;
|
|
558
|
+
}
|
|
559
|
+
case "blockStatement": {
|
|
560
|
+
for (const s of stmt.statements) {
|
|
561
|
+
processStatement(s, ctx);
|
|
562
|
+
}
|
|
563
|
+
break;
|
|
564
|
+
}
|
|
565
|
+
case "ifStatement": {
|
|
566
|
+
processStatement(stmt.thenStatement, ctx);
|
|
567
|
+
if (stmt.elseStatement) {
|
|
568
|
+
processStatement(stmt.elseStatement, ctx);
|
|
569
|
+
}
|
|
570
|
+
break;
|
|
571
|
+
}
|
|
572
|
+
case "whileStatement":
|
|
573
|
+
case "forStatement":
|
|
574
|
+
case "forOfStatement": {
|
|
575
|
+
processStatement(stmt.body, ctx);
|
|
576
|
+
break;
|
|
577
|
+
}
|
|
578
|
+
case "tryStatement": {
|
|
579
|
+
processStatement(stmt.tryBlock, ctx);
|
|
580
|
+
if (stmt.catchClause) {
|
|
581
|
+
processStatement(stmt.catchClause.body, ctx);
|
|
582
|
+
}
|
|
583
|
+
if (stmt.finallyBlock) {
|
|
584
|
+
processStatement(stmt.finallyBlock, ctx);
|
|
585
|
+
}
|
|
586
|
+
break;
|
|
587
|
+
}
|
|
588
|
+
case "switchStatement": {
|
|
589
|
+
for (const caseClause of stmt.cases) {
|
|
590
|
+
for (const s of caseClause.statements) {
|
|
591
|
+
processStatement(s, ctx);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
break;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
};
|
|
598
|
+
/**
|
|
599
|
+
* Process a statement with return type context for checking return statements
|
|
600
|
+
*/
|
|
601
|
+
const processStatementWithReturnType = (stmt, returnType, ctx) => {
|
|
602
|
+
switch (stmt.kind) {
|
|
603
|
+
case "returnStatement": {
|
|
604
|
+
if (stmt.expression && returnType) {
|
|
605
|
+
validateExpression(stmt.expression, returnType, ctx, "in return statement");
|
|
606
|
+
}
|
|
607
|
+
break;
|
|
608
|
+
}
|
|
609
|
+
case "blockStatement": {
|
|
610
|
+
for (const s of stmt.statements) {
|
|
611
|
+
processStatementWithReturnType(s, returnType, ctx);
|
|
612
|
+
}
|
|
613
|
+
break;
|
|
614
|
+
}
|
|
615
|
+
case "ifStatement": {
|
|
616
|
+
processStatementWithReturnType(stmt.thenStatement, returnType, ctx);
|
|
617
|
+
if (stmt.elseStatement) {
|
|
618
|
+
processStatementWithReturnType(stmt.elseStatement, returnType, ctx);
|
|
619
|
+
}
|
|
620
|
+
break;
|
|
621
|
+
}
|
|
622
|
+
case "tryStatement": {
|
|
623
|
+
processStatementWithReturnType(stmt.tryBlock, returnType, ctx);
|
|
624
|
+
if (stmt.catchClause) {
|
|
625
|
+
processStatementWithReturnType(stmt.catchClause.body, returnType, ctx);
|
|
626
|
+
}
|
|
627
|
+
if (stmt.finallyBlock) {
|
|
628
|
+
processStatementWithReturnType(stmt.finallyBlock, returnType, ctx);
|
|
629
|
+
}
|
|
630
|
+
break;
|
|
631
|
+
}
|
|
632
|
+
case "switchStatement": {
|
|
633
|
+
for (const caseClause of stmt.cases) {
|
|
634
|
+
for (const s of caseClause.statements) {
|
|
635
|
+
processStatementWithReturnType(s, returnType, ctx);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
break;
|
|
639
|
+
}
|
|
640
|
+
default:
|
|
641
|
+
// For other statements, use regular processing
|
|
642
|
+
processStatement(stmt, ctx);
|
|
643
|
+
}
|
|
644
|
+
};
|
|
645
|
+
/**
|
|
646
|
+
* Run numeric coercion pass on a module.
|
|
647
|
+
*/
|
|
648
|
+
const processModule = (module) => {
|
|
649
|
+
const ctx = {
|
|
650
|
+
filePath: module.filePath,
|
|
651
|
+
diagnostics: [],
|
|
652
|
+
};
|
|
653
|
+
// Process module body
|
|
654
|
+
for (const stmt of module.body) {
|
|
655
|
+
processStatement(stmt, ctx);
|
|
656
|
+
}
|
|
657
|
+
// Process exports
|
|
658
|
+
for (const exp of module.exports) {
|
|
659
|
+
if (exp.kind === "declaration") {
|
|
660
|
+
processStatement(exp.declaration, ctx);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
return {
|
|
664
|
+
ok: ctx.diagnostics.length === 0,
|
|
665
|
+
module,
|
|
666
|
+
diagnostics: ctx.diagnostics,
|
|
667
|
+
};
|
|
668
|
+
};
|
|
669
|
+
/**
|
|
670
|
+
* Run numeric coercion validation on all modules.
|
|
671
|
+
*
|
|
672
|
+
* HARD GATE: If any diagnostics are returned, the emitter MUST NOT run.
|
|
673
|
+
*/
|
|
674
|
+
export const runNumericCoercionPass = (modules) => {
|
|
675
|
+
const allDiagnostics = [];
|
|
676
|
+
for (const module of modules) {
|
|
677
|
+
const result = processModule(module);
|
|
678
|
+
allDiagnostics.push(...result.diagnostics);
|
|
679
|
+
}
|
|
680
|
+
return {
|
|
681
|
+
ok: allDiagnostics.length === 0,
|
|
682
|
+
modules, // Pass through unchanged - this pass only validates, doesn't transform
|
|
683
|
+
diagnostics: allDiagnostics,
|
|
684
|
+
};
|
|
685
|
+
};
|
|
686
|
+
//# sourceMappingURL=numeric-coercion-pass.js.map
|