stone-lang 0.1.0
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/README.md +52 -0
- package/StoneEngine.js +879 -0
- package/StoneEngineService.js +1727 -0
- package/adapters/FileSystemAdapter.js +230 -0
- package/adapters/OutputAdapter.js +208 -0
- package/adapters/index.js +6 -0
- package/cli/CLIOutputAdapter.js +196 -0
- package/cli/DaemonClient.js +349 -0
- package/cli/JSONOutputAdapter.js +135 -0
- package/cli/ReplSession.js +567 -0
- package/cli/ViewerServer.js +590 -0
- package/cli/commands/check.js +84 -0
- package/cli/commands/daemon.js +189 -0
- package/cli/commands/kill.js +66 -0
- package/cli/commands/package.js +713 -0
- package/cli/commands/ps.js +65 -0
- package/cli/commands/run.js +537 -0
- package/cli/entry.js +169 -0
- package/cli/index.js +14 -0
- package/cli/stonec.js +358 -0
- package/cli/test-compiler.js +181 -0
- package/cli/viewer/index.html +495 -0
- package/daemon/IPCServer.js +455 -0
- package/daemon/ProcessManager.js +327 -0
- package/daemon/ProcessRunner.js +307 -0
- package/daemon/daemon.js +398 -0
- package/daemon/index.js +16 -0
- package/frontend/analysis/index.js +5 -0
- package/frontend/analysis/livenessAnalyzer.js +568 -0
- package/frontend/analysis/treeShaker.js +265 -0
- package/frontend/index.js +20 -0
- package/frontend/parsing/astBuilder.js +2196 -0
- package/frontend/parsing/index.js +7 -0
- package/frontend/parsing/sonParser.js +592 -0
- package/frontend/parsing/stoneAstTypes.js +703 -0
- package/frontend/parsing/terminal-registry.js +435 -0
- package/frontend/parsing/tokenizer.js +692 -0
- package/frontend/type-checker/OverloadedFunctionType.js +43 -0
- package/frontend/type-checker/TypeEnvironment.js +165 -0
- package/frontend/type-checker/bidirectionalInference.js +149 -0
- package/frontend/type-checker/index.js +10 -0
- package/frontend/type-checker/moduleAnalysis.js +248 -0
- package/frontend/type-checker/operatorMappings.js +35 -0
- package/frontend/type-checker/overloadResolution.js +605 -0
- package/frontend/type-checker/typeChecker.js +452 -0
- package/frontend/type-checker/typeCompatibility.js +389 -0
- package/frontend/type-checker/visitors/controlFlow.js +483 -0
- package/frontend/type-checker/visitors/functions.js +604 -0
- package/frontend/type-checker/visitors/index.js +38 -0
- package/frontend/type-checker/visitors/literals.js +341 -0
- package/frontend/type-checker/visitors/modules.js +159 -0
- package/frontend/type-checker/visitors/operators.js +109 -0
- package/frontend/type-checker/visitors/statements.js +768 -0
- package/frontend/types/index.js +5 -0
- package/frontend/types/operatorMap.js +134 -0
- package/frontend/types/types.js +2046 -0
- package/frontend/utils/errorCollector.js +244 -0
- package/frontend/utils/index.js +5 -0
- package/frontend/utils/moduleResolver.js +479 -0
- package/package.json +50 -0
- package/packages/browserCache.js +359 -0
- package/packages/fetcher.js +236 -0
- package/packages/index.js +130 -0
- package/packages/lockfile.js +271 -0
- package/packages/manifest.js +291 -0
- package/packages/packageResolver.js +356 -0
- package/packages/resolver.js +310 -0
- package/packages/semver.js +635 -0
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Control Flow Visitors - mixin for TypeChecker
|
|
3
|
+
*
|
|
4
|
+
* Visitors for loops, branches, and control flow constructs.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
FunctionType,
|
|
9
|
+
RecordType,
|
|
10
|
+
ArrayType,
|
|
11
|
+
TypeVariable,
|
|
12
|
+
NUM,
|
|
13
|
+
UNIT,
|
|
14
|
+
freshTypeVar,
|
|
15
|
+
typeEquals,
|
|
16
|
+
unify,
|
|
17
|
+
formatType,
|
|
18
|
+
matchType,
|
|
19
|
+
canPromote,
|
|
20
|
+
fromTypeAnnotation,
|
|
21
|
+
} from "../../types/types.js";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Control flow visitor methods to be mixed into TypeChecker.prototype
|
|
25
|
+
*/
|
|
26
|
+
export const controlFlowVisitors = {
|
|
27
|
+
/**
|
|
28
|
+
* Visit a loop expression
|
|
29
|
+
* Validates that state variable transitions preserve types
|
|
30
|
+
*/
|
|
31
|
+
visitLoopExpression(node, env) {
|
|
32
|
+
const loopEnv = env.createChild();
|
|
33
|
+
const stateTypes = new Map();
|
|
34
|
+
|
|
35
|
+
// Helper: detect if an expression references a given identifier name
|
|
36
|
+
// Used to flag self-referential loop initializers (x = x) which evaluate to 'none'
|
|
37
|
+
const exprContainsIdentifier = (expr, name, parent = null, parentKey = null) => {
|
|
38
|
+
if (!expr || typeof expr !== "object") return false;
|
|
39
|
+
if (expr.type === "Identifier" && expr.name === name) {
|
|
40
|
+
// Ignore property positions like fwd.M (MemberExpression.property) where the identifier is a field name
|
|
41
|
+
const isMemberProp = (parent?.type === "MemberExpression" || parent?.type === "MemberAccess") &&
|
|
42
|
+
parent.property === expr &&
|
|
43
|
+
parent.computed !== true;
|
|
44
|
+
// Ignore identifier nodes that serve as object/record keys (common key property names)
|
|
45
|
+
const isObjectKey = typeof parentKey === "string" && /name|key|field/i.test(parentKey);
|
|
46
|
+
if (!isMemberProp && !isObjectKey) {
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
for (const key of Object.keys(expr)) {
|
|
51
|
+
const val = expr[key];
|
|
52
|
+
if (Array.isArray(val)) {
|
|
53
|
+
if (val.some(child => exprContainsIdentifier(child, name, expr, key))) return true;
|
|
54
|
+
} else if (val && typeof val === "object") {
|
|
55
|
+
if (exprContainsIdentifier(val, name, expr, key)) return true;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return false;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// Initialize state variables and their primed versions
|
|
62
|
+
for (const variable of node.variables) {
|
|
63
|
+
let varType;
|
|
64
|
+
// Disallow shadowing outer user-defined bindings (Stone has no shadowing)
|
|
65
|
+
// But allow loop state variables to have the same name as built-in functions
|
|
66
|
+
// (e.g., a loop state variable named 'sum' shouldn't conflict with the built-in sum function)
|
|
67
|
+
const existingBinding = env.lookup(variable.name);
|
|
68
|
+
if (existingBinding && !(existingBinding instanceof FunctionType)) {
|
|
69
|
+
this.addError(
|
|
70
|
+
`Loop variable '${variable.name}' shadows an outer binding. Stone disallows shadowing; use a distinct name.`,
|
|
71
|
+
variable.location || node.location
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
// Prevent self-referential loop initializers: h = h yields 'none' at runtime
|
|
75
|
+
const outerBinding = env.lookup(variable.name); // allow referencing an outer binding with same name
|
|
76
|
+
if (!outerBinding && variable.initialValue && exprContainsIdentifier(variable.initialValue, variable.name)) {
|
|
77
|
+
this.addError(
|
|
78
|
+
`Loop variable '${variable.name}' is initialized with itself. ` +
|
|
79
|
+
`Loop state initializers cannot reference their own name; compute the value in a separate binding (e.g., 'h_init') and use that instead.`,
|
|
80
|
+
variable.initialValue.location || node.location
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
if (variable.typeAnnotation) {
|
|
84
|
+
varType = fromTypeAnnotation(variable.typeAnnotation, env);
|
|
85
|
+
// If there's also an initial value, verify it matches the annotation
|
|
86
|
+
if (variable.initialValue) {
|
|
87
|
+
const initType = this.visit(variable.initialValue, env);
|
|
88
|
+
const unifyResult = unify(varType, initType);
|
|
89
|
+
if (!unifyResult.success) {
|
|
90
|
+
this.addError(
|
|
91
|
+
`Loop variable '${variable.name}' declared as ${varType}, but initialized with ${initType}`,
|
|
92
|
+
node.location
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
} else if (variable.initialValue) {
|
|
97
|
+
varType = this.visit(variable.initialValue, env);
|
|
98
|
+
} else {
|
|
99
|
+
varType = freshTypeVar();
|
|
100
|
+
}
|
|
101
|
+
stateTypes.set(variable.name, varType);
|
|
102
|
+
loopEnv.define(variable.name, varType);
|
|
103
|
+
// Define primed version with same type (x' must have same type as x)
|
|
104
|
+
loopEnv.define(variable.name + "'", varType);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Process drivers - track for back-propagation of element types
|
|
108
|
+
const forDrivers = []; // [{iterableType, elemTypeVar, pattern}]
|
|
109
|
+
if (node.drivers) {
|
|
110
|
+
for (const driver of node.drivers) {
|
|
111
|
+
if (driver.type === "for") {
|
|
112
|
+
// For driver introduces iteration variable
|
|
113
|
+
const iterableType = this.visit(driver.iterable, loopEnv);
|
|
114
|
+
let elemType;
|
|
115
|
+
if (iterableType instanceof ArrayType) {
|
|
116
|
+
elemType = iterableType.rank === 1
|
|
117
|
+
? iterableType.elementType
|
|
118
|
+
: new ArrayType(iterableType.elementType, iterableType.rank - 1);
|
|
119
|
+
} else {
|
|
120
|
+
// Unknown iterable type - use fresh TypeVariable and track for back-propagation
|
|
121
|
+
elemType = freshTypeVar();
|
|
122
|
+
if (iterableType instanceof TypeVariable) {
|
|
123
|
+
forDrivers.push({ iterableType, elemType, pattern: driver.pattern });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
loopEnv.define(driver.pattern, elemType);
|
|
127
|
+
}
|
|
128
|
+
// While driver doesn't introduce variables
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Type check body with state type validation
|
|
133
|
+
// Track transition types for each state variable to ensure consistency
|
|
134
|
+
const transitionTypes = new Map(); // baseName -> [actualType, ...]
|
|
135
|
+
const assignedPrimes = new Set(); // Track which x' have been assigned
|
|
136
|
+
|
|
137
|
+
// Create a custom environment that tracks prime variable references
|
|
138
|
+
// We'll use this to detect references to x' before assignment
|
|
139
|
+
const primeRefEnv = loopEnv.createChild();
|
|
140
|
+
|
|
141
|
+
// Helper to check if an expression references unassigned primed variables
|
|
142
|
+
const checkPrimeReferences = (exprNode, location) => {
|
|
143
|
+
if (!exprNode) return;
|
|
144
|
+
|
|
145
|
+
if (exprNode.type === "Identifier" && exprNode.name.endsWith("'")) {
|
|
146
|
+
const baseName = exprNode.name.slice(0, -1);
|
|
147
|
+
if (stateTypes.has(baseName) && !assignedPrimes.has(baseName)) {
|
|
148
|
+
this.addError(
|
|
149
|
+
`Cannot reference '${exprNode.name}' before it is assigned in the loop body. ` +
|
|
150
|
+
`State transition '${exprNode.name}' must be assigned before it can be read.`,
|
|
151
|
+
exprNode.location || location
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Recursively check child nodes
|
|
157
|
+
if (exprNode.left) checkPrimeReferences(exprNode.left, location);
|
|
158
|
+
if (exprNode.right) checkPrimeReferences(exprNode.right, location);
|
|
159
|
+
if (exprNode.operand) checkPrimeReferences(exprNode.operand, location);
|
|
160
|
+
if (exprNode.args) exprNode.args.forEach(arg => checkPrimeReferences(arg, location));
|
|
161
|
+
if (exprNode.value) checkPrimeReferences(exprNode.value, location);
|
|
162
|
+
if (exprNode.object) checkPrimeReferences(exprNode.object, location);
|
|
163
|
+
if (exprNode.property && exprNode.computed) checkPrimeReferences(exprNode.property, location);
|
|
164
|
+
if (exprNode.condition) checkPrimeReferences(exprNode.condition, location);
|
|
165
|
+
if (exprNode.elements) exprNode.elements.forEach(el => checkPrimeReferences(el, location));
|
|
166
|
+
if (exprNode.bindings) exprNode.bindings.forEach(b => checkPrimeReferences(b.value, location));
|
|
167
|
+
if (exprNode.trailingExpr) checkPrimeReferences(exprNode.trailingExpr, location);
|
|
168
|
+
if (exprNode.paths) {
|
|
169
|
+
exprNode.paths.forEach(path => {
|
|
170
|
+
if (path.condition) checkPrimeReferences(path.condition, location);
|
|
171
|
+
if (path.body) path.body.forEach(stmt => checkPrimeReferences(stmt, location));
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
for (const stmt of node.body) {
|
|
177
|
+
// Check for state variable transitions (assignments to primed variables)
|
|
178
|
+
if (stmt.type === "Assignment" && stmt.target?.endsWith?.("'")) {
|
|
179
|
+
const baseName = stmt.target.slice(0, -1);
|
|
180
|
+
if (stateTypes.has(baseName)) {
|
|
181
|
+
// Check for multiple assignments to the same x'
|
|
182
|
+
if (assignedPrimes.has(baseName)) {
|
|
183
|
+
this.addError(
|
|
184
|
+
`State variable '${baseName}'' is assigned multiple times in the loop body. ` +
|
|
185
|
+
`Each state variable can only have one transition per iteration.`,
|
|
186
|
+
stmt.location
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Check for references to unassigned primed variables in the RHS
|
|
191
|
+
checkPrimeReferences(stmt.value, stmt.location);
|
|
192
|
+
|
|
193
|
+
const expectedType = stateTypes.get(baseName);
|
|
194
|
+
const actualType = this.visit(stmt.value, loopEnv);
|
|
195
|
+
|
|
196
|
+
// Mark this prime as assigned AFTER checking the RHS
|
|
197
|
+
assignedPrimes.add(baseName);
|
|
198
|
+
|
|
199
|
+
// Track all transition types for this variable
|
|
200
|
+
if (!transitionTypes.has(baseName)) {
|
|
201
|
+
transitionTypes.set(baseName, []);
|
|
202
|
+
}
|
|
203
|
+
transitionTypes.get(baseName).push({ type: actualType, location: stmt.location });
|
|
204
|
+
|
|
205
|
+
// Unify the actual type with the expected type
|
|
206
|
+
// This ensures type inference works for empty arrays and catches mismatches
|
|
207
|
+
const unifyResult = unify(expectedType, actualType);
|
|
208
|
+
if (!unifyResult.success) {
|
|
209
|
+
const expectedStr = formatType(this.toStructuredType(expectedType));
|
|
210
|
+
const actualStr = formatType(this.toStructuredType(actualType));
|
|
211
|
+
this.addError(
|
|
212
|
+
`Loop state type mismatch: '${baseName}' is ${expectedStr}, ` +
|
|
213
|
+
`transition '${baseName}'' is ${actualStr}`,
|
|
214
|
+
stmt.location
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
continue;
|
|
218
|
+
} else {
|
|
219
|
+
// Check if this is an outer loop's state variable
|
|
220
|
+
// If the primed variable is accessible but not in current loop's state,
|
|
221
|
+
// it must be from an enclosing loop
|
|
222
|
+
const primedVarType = loopEnv.lookup(stmt.target);
|
|
223
|
+
if (primedVarType !== null) {
|
|
224
|
+
this.addError(
|
|
225
|
+
`Cannot update outer loop state variable '${baseName}' from inner loop. ` +
|
|
226
|
+
`State transitions (using '${baseName}\\'') only work for the current loop's state variables. ` +
|
|
227
|
+
`To accumulate values across nested loops, collect results in the inner loop and merge in the outer loop.`,
|
|
228
|
+
stmt.location
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// For non-prime assignments, also check for prime references
|
|
235
|
+
if (stmt.type === "Assignment" && stmt.value) {
|
|
236
|
+
checkPrimeReferences(stmt.value, stmt.location);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
this.visit(stmt, loopEnv);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Verify all transitions for each variable produce consistent types
|
|
243
|
+
for (const [baseName, transitions] of transitionTypes) {
|
|
244
|
+
if (transitions.length > 1) {
|
|
245
|
+
const firstType = transitions[0].type;
|
|
246
|
+
for (let i = 1; i < transitions.length; i++) {
|
|
247
|
+
const otherType = transitions[i].type;
|
|
248
|
+
// After unification, check if they resolved to the same type
|
|
249
|
+
const resolved1 = firstType instanceof TypeVariable ? firstType.resolve() : firstType;
|
|
250
|
+
const resolved2 = otherType instanceof TypeVariable ? otherType.resolve() : otherType;
|
|
251
|
+
if (!typeEquals(resolved1, resolved2)) {
|
|
252
|
+
// Types don't match - report error
|
|
253
|
+
const type1Str = formatType(this.toStructuredType(resolved1));
|
|
254
|
+
const type2Str = formatType(this.toStructuredType(resolved2));
|
|
255
|
+
this.addError(
|
|
256
|
+
`Inconsistent transition types for '${baseName}': ` +
|
|
257
|
+
`first assignment produces ${type1Str}, but another produces ${type2Str}`,
|
|
258
|
+
transitions[i].location
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Back-propagate element types to iterable TypeVariables
|
|
266
|
+
// If we learned the element type from usage, constrain the iterable
|
|
267
|
+
for (const { iterableType, elemType } of forDrivers) {
|
|
268
|
+
const resolvedElem = elemType instanceof TypeVariable ? elemType.resolve() : elemType;
|
|
269
|
+
if (resolvedElem !== elemType && !(resolvedElem instanceof TypeVariable)) {
|
|
270
|
+
// Element type was resolved - unify iterable with appropriate container type
|
|
271
|
+
// Default to 1D array since we don't know the rank
|
|
272
|
+
unify(iterableType, new ArrayType(resolvedElem, 1));
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Result is a record of state variables
|
|
277
|
+
const resultFields = new Map();
|
|
278
|
+
for (const [name, type] of stateTypes) {
|
|
279
|
+
resultFields.set(name, type);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return this.setType(node, new RecordType(resultFields, false));
|
|
283
|
+
},
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Detect if a condition is a discriminant check pattern.
|
|
287
|
+
* Patterns: x.field == "literal" or x.field != "literal"
|
|
288
|
+
* Returns { varName, fieldName, literal, isNegated } or null.
|
|
289
|
+
*/
|
|
290
|
+
extractDiscriminantCheck(condition) {
|
|
291
|
+
if (!condition) return null;
|
|
292
|
+
|
|
293
|
+
// Check for binary comparison (== or !=)
|
|
294
|
+
if (condition.type !== "BinaryOp") return null;
|
|
295
|
+
if (condition.operator !== "==" && condition.operator !== "!=") return null;
|
|
296
|
+
|
|
297
|
+
const left = condition.left;
|
|
298
|
+
const right = condition.right;
|
|
299
|
+
|
|
300
|
+
// Left side should be member access (x.field)
|
|
301
|
+
if (left?.type !== "MemberAccess" || left.computed) return null;
|
|
302
|
+
if (left.object?.type !== "Identifier") return null;
|
|
303
|
+
|
|
304
|
+
// Right side should be a string literal
|
|
305
|
+
if (right?.type !== "Literal" || typeof right.value !== "string") return null;
|
|
306
|
+
|
|
307
|
+
return {
|
|
308
|
+
varName: left.object.name,
|
|
309
|
+
fieldName: left.property?.name,
|
|
310
|
+
literal: right.value,
|
|
311
|
+
isNegated: condition.operator === "!="
|
|
312
|
+
};
|
|
313
|
+
},
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Check if a branch body is a simple return of a variable.
|
|
317
|
+
* Returns the variable name if so, null otherwise.
|
|
318
|
+
*/
|
|
319
|
+
getSimpleReturnVar(body) {
|
|
320
|
+
if (!body || body.length !== 1) return null;
|
|
321
|
+
const stmt = body[0];
|
|
322
|
+
|
|
323
|
+
// Check for return statement
|
|
324
|
+
if (stmt.type === "ReturnStatement" && stmt.value?.type === "Identifier") {
|
|
325
|
+
return stmt.value.name;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Check for direct identifier (implicit return in single-statement branch)
|
|
329
|
+
if (stmt.type === "Identifier") {
|
|
330
|
+
return stmt.name;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return null;
|
|
334
|
+
},
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Visit a branch expression (if/elif/else)
|
|
338
|
+
* Validates that all branches return compatible types
|
|
339
|
+
*/
|
|
340
|
+
visitBranchExpression(node, env) {
|
|
341
|
+
const branchResults = [];
|
|
342
|
+
const branchLabels = [];
|
|
343
|
+
let hasElseBranch = false;
|
|
344
|
+
|
|
345
|
+
// Detect discriminant guard pattern in first branch
|
|
346
|
+
// Pattern: if (x.field != "literal") { return x }
|
|
347
|
+
// This is a "type guard passthrough" - the branch returns the input unchanged
|
|
348
|
+
// when it's already the correct type, so we shouldn't unify its type with other branches
|
|
349
|
+
let guardPassthroughBranchIndex = -1;
|
|
350
|
+
|
|
351
|
+
if (node.paths.length >= 2 && node.paths[0].condition) {
|
|
352
|
+
const discriminant = this.extractDiscriminantCheck(node.paths[0].condition);
|
|
353
|
+
if (discriminant && discriminant.isNegated) {
|
|
354
|
+
// Check if branch body just returns the discriminated variable
|
|
355
|
+
const returnVar = this.getSimpleReturnVar(node.paths[0].body);
|
|
356
|
+
if (returnVar === discriminant.varName) {
|
|
357
|
+
guardPassthroughBranchIndex = 0;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
for (let i = 0; i < node.paths.length; i++) {
|
|
363
|
+
const path = node.paths[i];
|
|
364
|
+
if (path.condition) {
|
|
365
|
+
this.visit(path.condition, env);
|
|
366
|
+
} else {
|
|
367
|
+
// Path without condition is the else branch
|
|
368
|
+
hasElseBranch = true;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const branchEnv = env.createChild();
|
|
372
|
+
let lastType = UNIT;
|
|
373
|
+
|
|
374
|
+
for (const stmt of path.body) {
|
|
375
|
+
// Always visit the statement to ensure proper type tracking
|
|
376
|
+
lastType = this.visit(stmt, branchEnv);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
branchResults.push({
|
|
380
|
+
path,
|
|
381
|
+
type: lastType,
|
|
382
|
+
isIncomplete: this.isIncompleteType(lastType),
|
|
383
|
+
index: i
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
// Label for error messages
|
|
387
|
+
if (i === 0) {
|
|
388
|
+
branchLabels.push("'if'");
|
|
389
|
+
} else if (path.condition) {
|
|
390
|
+
branchLabels.push(`'elif' #${i}`);
|
|
391
|
+
} else {
|
|
392
|
+
branchLabels.push("'else'");
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Check for value-producing conditionals without else branch
|
|
397
|
+
if (node._isValueProducing && !hasElseBranch) {
|
|
398
|
+
this.addError(
|
|
399
|
+
`Conditional expression used as a value must have an 'else' branch. ` +
|
|
400
|
+
`Add 'else { ... }' to provide a value for all cases.`,
|
|
401
|
+
node.location
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Separate complete and incomplete types
|
|
406
|
+
// Exclude guard passthrough branch from unification - it returns the input unchanged
|
|
407
|
+
// so its type shouldn't constrain other branches' types
|
|
408
|
+
const complete = branchResults.filter(b => !b.isIncomplete && b.index !== guardPassthroughBranchIndex);
|
|
409
|
+
const incomplete = branchResults.filter(b => b.isIncomplete && b.index !== guardPassthroughBranchIndex);
|
|
410
|
+
|
|
411
|
+
// If there's only one branch (no else), this is a guard clause
|
|
412
|
+
// The if-expression itself returns UNIT; any return statements inside
|
|
413
|
+
// are handled at the function level
|
|
414
|
+
if (branchResults.length === 1) {
|
|
415
|
+
return this.setType(node, UNIT);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// If no branches at all, return UNIT
|
|
419
|
+
if (branchResults.length === 0) {
|
|
420
|
+
return this.setType(node, UNIT);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// If all branches are incomplete (type variables), unify them and return
|
|
424
|
+
// This handles polymorphic functions where parameter types are unknown
|
|
425
|
+
if (complete.length === 0 && incomplete.length > 0) {
|
|
426
|
+
let resultType = incomplete[0].type;
|
|
427
|
+
for (let i = 1; i < incomplete.length; i++) {
|
|
428
|
+
const result = unify(resultType, incomplete[i].type);
|
|
429
|
+
if (!result.success) {
|
|
430
|
+
const branchIdx = branchResults.indexOf(incomplete[i]);
|
|
431
|
+
const firstIdx = branchResults.indexOf(incomplete[0]);
|
|
432
|
+
this.addError(
|
|
433
|
+
`Branch type mismatch: ${branchLabels[firstIdx]} and ${branchLabels[branchIdx]} have incompatible types`,
|
|
434
|
+
node.location
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
if (resultType instanceof TypeVariable) {
|
|
438
|
+
resultType = resultType.resolve();
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
return this.setType(node, resultType);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Find common type from complete branches
|
|
445
|
+
let resultType = complete[0].type;
|
|
446
|
+
for (let i = 1; i < complete.length; i++) {
|
|
447
|
+
const result = unify(resultType, complete[i].type);
|
|
448
|
+
if (!result.success) {
|
|
449
|
+
// Find branch index for this complete branch
|
|
450
|
+
const branchIdx = branchResults.indexOf(complete[i]);
|
|
451
|
+
const firstIdx = branchResults.indexOf(complete[0]);
|
|
452
|
+
const firstStr = formatType(this.toStructuredType(resultType));
|
|
453
|
+
const otherStr = formatType(this.toStructuredType(complete[i].type));
|
|
454
|
+
this.addError(
|
|
455
|
+
`Branch type mismatch: ${branchLabels[firstIdx]} returns ${firstStr}, ` +
|
|
456
|
+
`${branchLabels[branchIdx]} returns ${otherStr}`,
|
|
457
|
+
node.location
|
|
458
|
+
);
|
|
459
|
+
} else {
|
|
460
|
+
// Resolve if it's a TypeVariable
|
|
461
|
+
if (resultType instanceof TypeVariable) {
|
|
462
|
+
resultType = resultType.resolve();
|
|
463
|
+
}
|
|
464
|
+
// When promotion happened, use the wider type (e.g., complex over num)
|
|
465
|
+
if (canPromote(resultType, complete[i].type).can) {
|
|
466
|
+
resultType = complete[i].type;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Back-propagate to incomplete branches
|
|
472
|
+
for (const { path, type } of incomplete) {
|
|
473
|
+
unify(type, resultType);
|
|
474
|
+
// Update the last statement's inferredType in the path
|
|
475
|
+
if (path.body.length > 0) {
|
|
476
|
+
const lastStmt = path.body[path.body.length - 1];
|
|
477
|
+
lastStmt.inferredType = resultType;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return this.setType(node, resultType);
|
|
482
|
+
},
|
|
483
|
+
};
|