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,768 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Statement Visitors - mixin for TypeChecker
|
|
3
|
+
*
|
|
4
|
+
* Visitors for assignments, destructuring, member access, ranges, etc.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
FunctionType,
|
|
9
|
+
RecordType,
|
|
10
|
+
ArrayType,
|
|
11
|
+
TypeVariable,
|
|
12
|
+
PrimitiveType,
|
|
13
|
+
NUM,
|
|
14
|
+
INT,
|
|
15
|
+
STRING,
|
|
16
|
+
UNIT,
|
|
17
|
+
freshTypeVar,
|
|
18
|
+
unify,
|
|
19
|
+
formatType,
|
|
20
|
+
fromTypeAnnotation,
|
|
21
|
+
} from "../../types/types.js";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Statement visitor methods to be mixed into TypeChecker.prototype
|
|
25
|
+
*/
|
|
26
|
+
export const statementVisitors = {
|
|
27
|
+
/**
|
|
28
|
+
* Visit an assignment
|
|
29
|
+
*/
|
|
30
|
+
visitAssignment(node, env) {
|
|
31
|
+
// Check for immutable variable rebinding (but allow primed variables like x')
|
|
32
|
+
// Skip this check for loop destructuring - the target in that case is a field name, not a binding
|
|
33
|
+
const targetName = node.target;
|
|
34
|
+
const isPrimedVariable = targetName && targetName.endsWith("'");
|
|
35
|
+
const isLoopDestructuring = node.value?.type === "LoopExpression" && node.value.destructuring;
|
|
36
|
+
if (!isPrimedVariable && !isLoopDestructuring && env.isImmutable(targetName)) {
|
|
37
|
+
this.addError(
|
|
38
|
+
`Cannot rebind immutable variable '${targetName}'. Variables in Stone are immutable once assigned.`,
|
|
39
|
+
node.location
|
|
40
|
+
);
|
|
41
|
+
// Continue type checking for error recovery
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Mark conditionals as value-producing (requires else branch)
|
|
45
|
+
if (node.value?.type === "BranchExpression") {
|
|
46
|
+
node.value._isValueProducing = true;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const valueType = this.visit(node.value, env);
|
|
50
|
+
|
|
51
|
+
// Handle loop destructuring: sumSq := loop(sumSq = 0) ~for(...) {...}
|
|
52
|
+
// The parser attaches destructuring info to the loop expression
|
|
53
|
+
// := ALWAYS destructures by field name (even for single target)
|
|
54
|
+
if (node.value?.type === "LoopExpression" && node.value.destructuring) {
|
|
55
|
+
const destructuring = node.value.destructuring;
|
|
56
|
+
|
|
57
|
+
// If value is a record, extract the destructured fields
|
|
58
|
+
if (valueType instanceof RecordType) {
|
|
59
|
+
// Extract each field by name
|
|
60
|
+
for (const target of destructuring) {
|
|
61
|
+
const fieldType = valueType.fields.get(target.name);
|
|
62
|
+
const varName = target.alias || target.name;
|
|
63
|
+
if (fieldType) {
|
|
64
|
+
env.define(varName, fieldType);
|
|
65
|
+
env.markImmutable(varName);
|
|
66
|
+
} else {
|
|
67
|
+
// All records are closed - missing field is an error
|
|
68
|
+
const availableFields = Array.from(valueType.fields.keys()).join(", ");
|
|
69
|
+
this.addError(
|
|
70
|
+
`Field '${target.name}' does not exist on loop result. ` +
|
|
71
|
+
`':=' destructures by field name. Available fields: ${availableFields}. ` +
|
|
72
|
+
`Use '=' instead to bind the entire loop state object to a single name.`,
|
|
73
|
+
target.location || node.location
|
|
74
|
+
);
|
|
75
|
+
env.define(varName, freshTypeVar());
|
|
76
|
+
env.markImmutable(varName);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// For single-field destructuring, the assigned variable gets the field type
|
|
81
|
+
if (destructuring.length === 1) {
|
|
82
|
+
const fieldType = valueType.fields.get(destructuring[0].name);
|
|
83
|
+
if (fieldType) {
|
|
84
|
+
return this.setType(node, fieldType);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return this.setType(node, UNIT);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// If there's a type annotation, check it matches
|
|
93
|
+
if (node.typeAnnotation) {
|
|
94
|
+
let annotatedType = fromTypeAnnotation(node.typeAnnotation, env);
|
|
95
|
+
|
|
96
|
+
// If annotatedType is a fresh TypeVariable, check if the annotation references
|
|
97
|
+
// a schema record variable (e.g., user1: userType where userType is a record)
|
|
98
|
+
let isSchemaRecord = false;
|
|
99
|
+
if (annotatedType instanceof TypeVariable && !annotatedType.resolved) {
|
|
100
|
+
if (node.typeAnnotation.kind === 'simple') {
|
|
101
|
+
const schemaName = node.typeAnnotation.details?.name || node.typeAnnotation.baseType;
|
|
102
|
+
if (schemaName) {
|
|
103
|
+
// Check if this variable exists in the environment
|
|
104
|
+
const schemaType = env.lookup(schemaName);
|
|
105
|
+
if (schemaType instanceof RecordType) {
|
|
106
|
+
// The schema is a record type - use it as the annotated type
|
|
107
|
+
// This enables: userType = {username: string, role: string = 'guest'}
|
|
108
|
+
// user1: userType = {username = 'admin'}
|
|
109
|
+
annotatedType = schemaType;
|
|
110
|
+
isSchemaRecord = true;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Special case: schema record with defaults
|
|
117
|
+
// When using a schema record as type annotation, the value may have fewer fields
|
|
118
|
+
// because defaults will be applied at runtime by APPLY_SCHEMA
|
|
119
|
+
if (isSchemaRecord && annotatedType instanceof RecordType && valueType instanceof RecordType) {
|
|
120
|
+
// Check that all fields in the value match the schema
|
|
121
|
+
let allFieldsValid = true;
|
|
122
|
+
for (const [fieldName, fieldType] of valueType.fields) {
|
|
123
|
+
const schemaFieldType = annotatedType.fields.get(fieldName);
|
|
124
|
+
if (!schemaFieldType) {
|
|
125
|
+
this.addError(
|
|
126
|
+
`Field '${fieldName}' does not exist in schema '${node.typeAnnotation.details?.name || node.typeAnnotation.baseType}'`,
|
|
127
|
+
node.location
|
|
128
|
+
);
|
|
129
|
+
allFieldsValid = false;
|
|
130
|
+
} else {
|
|
131
|
+
const fieldUnifyResult = unify(schemaFieldType, fieldType);
|
|
132
|
+
if (!fieldUnifyResult.success) {
|
|
133
|
+
this.addError(
|
|
134
|
+
`Type mismatch for field '${fieldName}': expected ${formatType(schemaFieldType)}, got ${formatType(fieldType)}`,
|
|
135
|
+
node.location
|
|
136
|
+
);
|
|
137
|
+
allFieldsValid = false;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// The variable gets an instance type derived from the schema
|
|
142
|
+
// (same fields, but without requiredFields so field access is allowed)
|
|
143
|
+
const instanceType = new RecordType(
|
|
144
|
+
annotatedType.fields, // Same fields
|
|
145
|
+
false, // Not open
|
|
146
|
+
false, // Not explicit
|
|
147
|
+
null // No required fields - this is an instance, not a schema
|
|
148
|
+
);
|
|
149
|
+
env.define(node.target, instanceType);
|
|
150
|
+
if (!isPrimedVariable) {
|
|
151
|
+
env.markImmutable(node.target);
|
|
152
|
+
}
|
|
153
|
+
return this.setType(node, instanceType);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Special case: empty array with annotation (e.g., arr: array<complex,1> = [])
|
|
157
|
+
// Resolve the fresh TypeVariable from [] to match the declared element type
|
|
158
|
+
if (annotatedType instanceof ArrayType &&
|
|
159
|
+
valueType instanceof ArrayType &&
|
|
160
|
+
valueType.elementType instanceof TypeVariable &&
|
|
161
|
+
!valueType.elementType.resolved) {
|
|
162
|
+
// Unify the fresh TypeVariable with the declared element type
|
|
163
|
+
unify(valueType.elementType, annotatedType.elementType);
|
|
164
|
+
// Store the declared type for push validation
|
|
165
|
+
env.defineDeclared(node.target, annotatedType);
|
|
166
|
+
env.define(node.target, annotatedType);
|
|
167
|
+
if (!isPrimedVariable) {
|
|
168
|
+
env.markImmutable(node.target);
|
|
169
|
+
}
|
|
170
|
+
return this.setType(node, annotatedType);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const unifyResult = unify(annotatedType, valueType);
|
|
174
|
+
if (!unifyResult.success) {
|
|
175
|
+
this.addError(
|
|
176
|
+
`Type mismatch: variable '${node.target}' declared as ${annotatedType}, but assigned ${valueType}`,
|
|
177
|
+
node.location
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
// Also store declared type for non-empty arrays with annotations
|
|
181
|
+
if (annotatedType instanceof ArrayType) {
|
|
182
|
+
env.defineDeclared(node.target, annotatedType);
|
|
183
|
+
}
|
|
184
|
+
env.define(node.target, annotatedType);
|
|
185
|
+
if (!isPrimedVariable) {
|
|
186
|
+
env.markImmutable(node.target);
|
|
187
|
+
}
|
|
188
|
+
return this.setType(node, annotatedType);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
env.define(node.target, valueType);
|
|
192
|
+
if (!isPrimedVariable) {
|
|
193
|
+
env.markImmutable(node.target);
|
|
194
|
+
}
|
|
195
|
+
return this.setType(node, valueType);
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Visit a destructuring assignment
|
|
200
|
+
*/
|
|
201
|
+
visitDestructuringAssignment(node, env) {
|
|
202
|
+
const valueType = this.visit(node.value, env);
|
|
203
|
+
|
|
204
|
+
// Value should be a record type
|
|
205
|
+
if (!(valueType instanceof RecordType)) {
|
|
206
|
+
// Check if it's a concrete non-record type (not a TypeVariable)
|
|
207
|
+
if (!(valueType instanceof TypeVariable)) {
|
|
208
|
+
const typeStr = formatType(this.toStructuredType(valueType));
|
|
209
|
+
this.addError(
|
|
210
|
+
`Destructuring operator ':=' requires an object type, but got '${typeStr}'. ` +
|
|
211
|
+
`Arrays and primitives cannot be destructured with ':='.`,
|
|
212
|
+
node.location
|
|
213
|
+
);
|
|
214
|
+
// Define variables for error recovery
|
|
215
|
+
for (const target of node.targets) {
|
|
216
|
+
const name = target.alias || target.name;
|
|
217
|
+
env.define(name, freshTypeVar());
|
|
218
|
+
env.markImmutable(name);
|
|
219
|
+
}
|
|
220
|
+
return this.setType(node, UNIT);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// TypeVariable case: constrain it to be a record with the destructured fields
|
|
224
|
+
const fields = new Map();
|
|
225
|
+
for (const target of node.targets) {
|
|
226
|
+
const name = target.alias || target.name;
|
|
227
|
+
const fieldTypeVar = freshTypeVar();
|
|
228
|
+
fields.set(target.name, fieldTypeVar); // Use target.name as field name (what we extract)
|
|
229
|
+
env.define(name, fieldTypeVar);
|
|
230
|
+
env.markImmutable(name);
|
|
231
|
+
}
|
|
232
|
+
const recordConstraint = new RecordType(fields, false, true); // explicit constraint from destructuring
|
|
233
|
+
unify(valueType, recordConstraint);
|
|
234
|
+
return this.setType(node, UNIT);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Extract fields from known RecordType
|
|
238
|
+
for (const target of node.targets) {
|
|
239
|
+
const name = target.alias || target.name;
|
|
240
|
+
const fieldType = valueType.fields.get(target.name);
|
|
241
|
+
if (!fieldType) {
|
|
242
|
+
// All records are closed - missing field is an error
|
|
243
|
+
this.addError(
|
|
244
|
+
`Field '${target.name}' does not exist on record`,
|
|
245
|
+
target.location || node.location
|
|
246
|
+
);
|
|
247
|
+
env.define(name, freshTypeVar());
|
|
248
|
+
env.markImmutable(name);
|
|
249
|
+
} else {
|
|
250
|
+
env.define(name, fieldType);
|
|
251
|
+
env.markImmutable(name);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return this.setType(node, UNIT);
|
|
256
|
+
},
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Visit an indexed assignment (mapping)
|
|
260
|
+
*/
|
|
261
|
+
visitIndexedAssignment(node, env) {
|
|
262
|
+
// Create environment with index variables
|
|
263
|
+
const mappingEnv = env.createChild();
|
|
264
|
+
for (const index of node.indices) {
|
|
265
|
+
mappingEnv.define(index, INT);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Track which indices are actually used so we don't double-warn
|
|
269
|
+
const unusedIndices = new Set();
|
|
270
|
+
|
|
271
|
+
// Check if each index variable is used in the RHS expression
|
|
272
|
+
for (const indexName of node.indices) {
|
|
273
|
+
if (!this.containsIdentifier(node.value, indexName)) {
|
|
274
|
+
const indicesStr = node.indices.join("][");
|
|
275
|
+
this.addError(
|
|
276
|
+
`Index variable '${indexName}' in mapping '${node.target}[${indicesStr}]' is not used in the expression. ` +
|
|
277
|
+
`The result won't vary with '${indexName}'.`,
|
|
278
|
+
node.location
|
|
279
|
+
);
|
|
280
|
+
unusedIndices.add(indexName);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Additional validation: ensure each index drives at least one array access
|
|
285
|
+
if (unusedIndices.size === 0) {
|
|
286
|
+
if (node.indices.length === 1) {
|
|
287
|
+
const indexName = node.indices[0];
|
|
288
|
+
if (!this.findIndexedArray(node.value, indexName)) {
|
|
289
|
+
this.addError(
|
|
290
|
+
`No array found using index '${indexName}'`,
|
|
291
|
+
node.location
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
} else {
|
|
295
|
+
// Multi-dimensional mapping: either a single N-D array or one array per index
|
|
296
|
+
const ndArray = this.findNDIndexedArray(node.value, node.indices);
|
|
297
|
+
const arrayNodes = node.indices.map(idx => this.findIndexedArray(node.value, idx));
|
|
298
|
+
const hasAnyArrayNode = arrayNodes.some(node => node !== null);
|
|
299
|
+
|
|
300
|
+
if (!ndArray && !hasAnyArrayNode) {
|
|
301
|
+
this.addError(
|
|
302
|
+
`No arrays found using indices '${node.indices.join("', '")}'`,
|
|
303
|
+
node.location
|
|
304
|
+
);
|
|
305
|
+
} else if (!ndArray) {
|
|
306
|
+
for (let i = 0; i < node.indices.length; i++) {
|
|
307
|
+
if (arrayNodes[i] === null) {
|
|
308
|
+
const indexName = node.indices[i];
|
|
309
|
+
this.addError(
|
|
310
|
+
`Index '${indexName}' on left side has no corresponding indexed array on right side. ` +
|
|
311
|
+
`All indices must appear in indexed expressions (e.g., arr[${indexName}]).`,
|
|
312
|
+
node.location
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const valueType = this.visit(node.value, mappingEnv);
|
|
321
|
+
|
|
322
|
+
// Result is an array
|
|
323
|
+
const rank = node.indices.length;
|
|
324
|
+
// Result is always an array with the value type as element type
|
|
325
|
+
const resultType = new ArrayType(valueType, rank);
|
|
326
|
+
|
|
327
|
+
env.define(node.target, resultType);
|
|
328
|
+
env.markImmutable(node.target);
|
|
329
|
+
return this.setType(node, resultType);
|
|
330
|
+
},
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Check if an identifier name appears in an expression AST
|
|
334
|
+
*/
|
|
335
|
+
containsIdentifier(node, name) {
|
|
336
|
+
if (!node) return false;
|
|
337
|
+
|
|
338
|
+
// Check this node
|
|
339
|
+
if (node.type === "Identifier" && node.name === name) return true;
|
|
340
|
+
|
|
341
|
+
// Recursively check children
|
|
342
|
+
if (node.left && this.containsIdentifier(node.left, name)) return true;
|
|
343
|
+
if (node.right && this.containsIdentifier(node.right, name)) return true;
|
|
344
|
+
if (node.operand && this.containsIdentifier(node.operand, name)) return true;
|
|
345
|
+
if (node.args && node.args.some(arg => this.containsIdentifier(arg, name))) return true;
|
|
346
|
+
if (node.value && this.containsIdentifier(node.value, name)) return true;
|
|
347
|
+
if (node.object && this.containsIdentifier(node.object, name)) return true;
|
|
348
|
+
if (node.property && node.computed && this.containsIdentifier(node.property, name)) return true;
|
|
349
|
+
if (node.condition && this.containsIdentifier(node.condition, name)) return true;
|
|
350
|
+
if (node.elements && node.elements.some(el => this.containsIdentifier(el, name))) return true;
|
|
351
|
+
if (node.bindings && node.bindings.some(b => this.containsIdentifier(b.value, name))) return true;
|
|
352
|
+
if (node.trailingExpr && this.containsIdentifier(node.trailingExpr, name)) return true;
|
|
353
|
+
if (node.paths) {
|
|
354
|
+
for (const path of node.paths) {
|
|
355
|
+
if (path.condition && this.containsIdentifier(path.condition, name)) return true;
|
|
356
|
+
if (path.body && path.body.some(stmt => this.containsIdentifier(stmt, name))) return true;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
if (node.callee && this.containsIdentifier(node.callee, name)) return true;
|
|
360
|
+
|
|
361
|
+
return false;
|
|
362
|
+
},
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Find the first array that is indexed by the given variable in an expression.
|
|
366
|
+
* Mirrors the compiler's search so type errors align with compile-time failures.
|
|
367
|
+
*/
|
|
368
|
+
findIndexedArray(node, indexVar) {
|
|
369
|
+
if (!node) return null;
|
|
370
|
+
|
|
371
|
+
// Check if this is an indexed access like arr[i]
|
|
372
|
+
if (node.type === "MemberAccess" && node.computed) {
|
|
373
|
+
if (node.property?.type === "Identifier" && node.property.name === indexVar) {
|
|
374
|
+
return node.object;
|
|
375
|
+
}
|
|
376
|
+
// Could be nested like obj.field[i], recurse into object
|
|
377
|
+
const found = this.findIndexedArray(node.object, indexVar);
|
|
378
|
+
if (found) return found;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Recurse into binary operations
|
|
382
|
+
if (node.type === "BinaryOp") {
|
|
383
|
+
const left = this.findIndexedArray(node.left, indexVar);
|
|
384
|
+
if (left) return left;
|
|
385
|
+
return this.findIndexedArray(node.right, indexVar);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Recurse into unary operations
|
|
389
|
+
if (node.type === "UnaryOp") {
|
|
390
|
+
return this.findIndexedArray(node.operand, indexVar);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Recurse into function calls
|
|
394
|
+
if (node.type === "FunctionCall") {
|
|
395
|
+
for (const arg of node.args || []) {
|
|
396
|
+
const found = this.findIndexedArray(arg, indexVar);
|
|
397
|
+
if (found) return found;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Recurse into non-computed member access (e.g., points[i].x)
|
|
402
|
+
if (node.type === "MemberAccess" && !node.computed) {
|
|
403
|
+
return this.findIndexedArray(node.object, indexVar);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Recurse into array literals (e.g., [x[i], y[i]])
|
|
407
|
+
if (node.type === "ArrayLiteral") {
|
|
408
|
+
for (const elem of node.elements || []) {
|
|
409
|
+
const found = this.findIndexedArray(elem, indexVar);
|
|
410
|
+
if (found) return found;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Recurse into record/object literals
|
|
415
|
+
if (node.type === "RecordLiteral" || node.type === "ObjectLiteral") {
|
|
416
|
+
for (const field of node.fields || node.properties || []) {
|
|
417
|
+
const valueNode = field.value ?? field;
|
|
418
|
+
const found = this.findIndexedArray(valueNode, indexVar);
|
|
419
|
+
if (found) return found;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Recurse into ternary/conditional expressions
|
|
424
|
+
if (node.type === "ConditionalExpression" || node.type === "TernaryOp") {
|
|
425
|
+
const found = this.findIndexedArray(node.consequent, indexVar);
|
|
426
|
+
if (found) return found;
|
|
427
|
+
return this.findIndexedArray(node.alternate, indexVar);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Recurse into branch expressions (if/elif/else)
|
|
431
|
+
if (node.type === "BranchExpression") {
|
|
432
|
+
for (const path of node.paths || []) {
|
|
433
|
+
if (path.condition) {
|
|
434
|
+
const found = this.findIndexedArray(path.condition, indexVar);
|
|
435
|
+
if (found) return found;
|
|
436
|
+
}
|
|
437
|
+
for (const stmt of path.body || []) {
|
|
438
|
+
const found = this.findIndexedArray(stmt, indexVar);
|
|
439
|
+
if (found) return found;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
return null;
|
|
445
|
+
},
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Extract chained index accesses from a node, e.g., arr[i][j][k].
|
|
449
|
+
*/
|
|
450
|
+
extractIndexChain(node, targetIndices) {
|
|
451
|
+
let current = node;
|
|
452
|
+
const indices = [];
|
|
453
|
+
|
|
454
|
+
while (current && current.type === "MemberAccess" && current.computed) {
|
|
455
|
+
if (current.property?.type === "Identifier") {
|
|
456
|
+
const idx = current.property.name;
|
|
457
|
+
if (targetIndices.includes(idx)) {
|
|
458
|
+
indices.unshift(idx);
|
|
459
|
+
current = current.object;
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
break;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (indices.length >= 2) {
|
|
467
|
+
const uniqueIndices = new Set(indices);
|
|
468
|
+
if (uniqueIndices.size === indices.length) {
|
|
469
|
+
return { array: current, indices };
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return null;
|
|
474
|
+
},
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Find an N-D indexed array that uses all target indices (supports permutations).
|
|
478
|
+
*/
|
|
479
|
+
findNDIndexedArray(node, targetIndices) {
|
|
480
|
+
if (!node) return null;
|
|
481
|
+
|
|
482
|
+
const extracted = this.extractIndexChain(node, targetIndices);
|
|
483
|
+
if (extracted && extracted.indices.length === targetIndices.length) {
|
|
484
|
+
return extracted;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if (node.type === "BinaryOp") {
|
|
488
|
+
const left = this.findNDIndexedArray(node.left, targetIndices);
|
|
489
|
+
if (left) return left;
|
|
490
|
+
return this.findNDIndexedArray(node.right, targetIndices);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if (node.type === "UnaryOp") {
|
|
494
|
+
return this.findNDIndexedArray(node.operand, targetIndices);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
if (node.type === "FunctionCall") {
|
|
498
|
+
for (const arg of node.args || []) {
|
|
499
|
+
const found = this.findNDIndexedArray(arg, targetIndices);
|
|
500
|
+
if (found) return found;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (node.type === "MemberAccess" && !node.computed) {
|
|
505
|
+
return this.findNDIndexedArray(node.object, targetIndices);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
if (node.type === "ArrayLiteral") {
|
|
509
|
+
for (const elem of node.elements || []) {
|
|
510
|
+
const found = this.findNDIndexedArray(elem, targetIndices);
|
|
511
|
+
if (found) return found;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
if (node.type === "RecordLiteral" || node.type === "ObjectLiteral") {
|
|
516
|
+
for (const field of node.fields || node.properties || []) {
|
|
517
|
+
const valueNode = field.value ?? field;
|
|
518
|
+
const found = this.findNDIndexedArray(valueNode, targetIndices);
|
|
519
|
+
if (found) return found;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
if (node.type === "ConditionalExpression" || node.type === "TernaryOp") {
|
|
524
|
+
const found = this.findNDIndexedArray(node.consequent, targetIndices);
|
|
525
|
+
if (found) return found;
|
|
526
|
+
return this.findNDIndexedArray(node.alternate, targetIndices);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (node.type === "BranchExpression") {
|
|
530
|
+
for (const path of node.paths || []) {
|
|
531
|
+
if (path.condition) {
|
|
532
|
+
const found = this.findNDIndexedArray(path.condition, targetIndices);
|
|
533
|
+
if (found) return found;
|
|
534
|
+
}
|
|
535
|
+
for (const stmt of path.body || []) {
|
|
536
|
+
const found = this.findNDIndexedArray(stmt, targetIndices);
|
|
537
|
+
if (found) return found;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
return null;
|
|
543
|
+
},
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Visit a return statement
|
|
547
|
+
*/
|
|
548
|
+
visitReturnStatement(node, env) {
|
|
549
|
+
if (node.value) {
|
|
550
|
+
const valueType = this.visit(node.value, env);
|
|
551
|
+
return this.setType(node, valueType);
|
|
552
|
+
}
|
|
553
|
+
return this.setType(node, UNIT);
|
|
554
|
+
},
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Visit a member access
|
|
558
|
+
*/
|
|
559
|
+
visitMemberAccess(node, env) {
|
|
560
|
+
const objectType = this.visit(node.object, env);
|
|
561
|
+
|
|
562
|
+
if (node.computed) {
|
|
563
|
+
// Array indexing: arr[i]
|
|
564
|
+
if (objectType instanceof ArrayType) {
|
|
565
|
+
if (objectType.rank === 1) {
|
|
566
|
+
return this.setType(node, objectType.elementType);
|
|
567
|
+
}
|
|
568
|
+
return this.setType(node, new ArrayType(objectType.elementType, objectType.rank - 1));
|
|
569
|
+
}
|
|
570
|
+
return this.setType(node, freshTypeVar());
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// Property access: obj.field
|
|
574
|
+
const propName = node.property.name;
|
|
575
|
+
|
|
576
|
+
// Array methods - return FunctionType for method calls
|
|
577
|
+
if (objectType instanceof ArrayType) {
|
|
578
|
+
// For arrays, the "element" for push is the actual element type (rank 1)
|
|
579
|
+
// or a sub-array with rank-1 (higher ranks)
|
|
580
|
+
let pushElemType;
|
|
581
|
+
if (objectType.rank === 1) {
|
|
582
|
+
pushElemType = objectType.elementType;
|
|
583
|
+
} else {
|
|
584
|
+
// For rank > 1, push accepts sub-array with rank-1
|
|
585
|
+
pushElemType = new ArrayType(objectType.elementType, objectType.rank - 1);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
const elemType = objectType.elementType;
|
|
589
|
+
|
|
590
|
+
// If element type is unresolved, check for declared type from annotation
|
|
591
|
+
// This handles: arr: array<num,1> = [] followed by arr.push(x)
|
|
592
|
+
if (pushElemType instanceof TypeVariable && !pushElemType.resolved) {
|
|
593
|
+
// Try to get the array variable name for declared type lookup
|
|
594
|
+
if (node.object && node.object.type === "Identifier") {
|
|
595
|
+
const arrayVarName = node.object.name;
|
|
596
|
+
// lookupDeclared already handles primed variables (arr' → arr)
|
|
597
|
+
const declaredType = env.lookupDeclared(arrayVarName);
|
|
598
|
+
if (declaredType instanceof ArrayType) {
|
|
599
|
+
if (objectType.rank === 1) {
|
|
600
|
+
pushElemType = declaredType.elementType;
|
|
601
|
+
} else {
|
|
602
|
+
pushElemType = new ArrayType(declaredType.elementType, objectType.rank - 1);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
switch (propName) {
|
|
609
|
+
case 'push':
|
|
610
|
+
// push(item) -> same container type with item appended
|
|
611
|
+
return this.setType(node, new FunctionType([pushElemType], objectType));
|
|
612
|
+
case 'pop':
|
|
613
|
+
// pop() -> {array: same type, value: elem type}
|
|
614
|
+
const popResultFields = new Map();
|
|
615
|
+
popResultFields.set('array', objectType);
|
|
616
|
+
popResultFields.set('value', elemType);
|
|
617
|
+
return this.setType(node, new FunctionType([], new RecordType(popResultFields, false)));
|
|
618
|
+
case 'concat':
|
|
619
|
+
// concat(other) -> same container type
|
|
620
|
+
return this.setType(node, new FunctionType([objectType], objectType));
|
|
621
|
+
case 'slice':
|
|
622
|
+
// slice(start, end) -> same container type
|
|
623
|
+
return this.setType(node, new FunctionType([NUM, NUM], objectType));
|
|
624
|
+
case 'length':
|
|
625
|
+
// length property
|
|
626
|
+
return this.setType(node, NUM);
|
|
627
|
+
default:
|
|
628
|
+
// Check for imported extension methods
|
|
629
|
+
const arrayExtension = env.lookupExtension(propName);
|
|
630
|
+
if (arrayExtension) {
|
|
631
|
+
// Extension found - return its function type (with self already applied)
|
|
632
|
+
// The extension's functionType has self as first param, so we return a FunctionType
|
|
633
|
+
// that expects the remaining params and returns the same return type
|
|
634
|
+
const extFuncType = arrayExtension.functionType;
|
|
635
|
+
if (extFuncType instanceof FunctionType) {
|
|
636
|
+
// Create a function type without the self parameter (already bound to objectType)
|
|
637
|
+
const remainingParams = extFuncType.paramTypes.slice(1);
|
|
638
|
+
return this.setType(node, new FunctionType(remainingParams, extFuncType.returnType));
|
|
639
|
+
}
|
|
640
|
+
return this.setType(node, freshTypeVar());
|
|
641
|
+
}
|
|
642
|
+
// Unknown property/method on array - report error
|
|
643
|
+
this.addError(
|
|
644
|
+
`Property '${propName}' does not exist on arrays. Available: push, pop, concat, slice, length`,
|
|
645
|
+
node.location
|
|
646
|
+
);
|
|
647
|
+
return this.setType(node, freshTypeVar());
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
if (objectType instanceof RecordType) {
|
|
652
|
+
const fieldType = objectType.fields.get(propName);
|
|
653
|
+
const recordExtension = env.lookupExtension(propName);
|
|
654
|
+
|
|
655
|
+
// Check for conflict: both field and extension exist
|
|
656
|
+
if (fieldType && recordExtension) {
|
|
657
|
+
this.addError(
|
|
658
|
+
`Ambiguous access '${propName}': both an object property and an imported extension method exist. ` +
|
|
659
|
+
`Use bracket syntax obj["${propName}"] for property access, or ${propName}(obj) for the extension function.`,
|
|
660
|
+
node.location
|
|
661
|
+
);
|
|
662
|
+
return this.setType(node, freshTypeVar());
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
if (fieldType) {
|
|
666
|
+
// Check if this is a schema record and the field is required (no default)
|
|
667
|
+
if (objectType.requiredFields && objectType.requiredFields.has(propName)) {
|
|
668
|
+
this.addError(
|
|
669
|
+
`Cannot access field '${propName}': it is a required type descriptor with no default value. ` +
|
|
670
|
+
`Use this schema as a type annotation (e.g., 'x: schemaName = {...}') to create instances.`,
|
|
671
|
+
node.location
|
|
672
|
+
);
|
|
673
|
+
return this.setType(node, freshTypeVar());
|
|
674
|
+
}
|
|
675
|
+
return this.setType(node, fieldType);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// Check for extension method on object
|
|
679
|
+
if (recordExtension) {
|
|
680
|
+
const extFuncType = recordExtension.functionType;
|
|
681
|
+
if (extFuncType instanceof FunctionType) {
|
|
682
|
+
const remainingParams = extFuncType.paramTypes.slice(1);
|
|
683
|
+
return this.setType(node, new FunctionType(remainingParams, extFuncType.returnType));
|
|
684
|
+
}
|
|
685
|
+
return this.setType(node, freshTypeVar());
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// All records are closed - unknown field is an error
|
|
689
|
+
this.addError(
|
|
690
|
+
`Field '${propName}' does not exist on record`,
|
|
691
|
+
node.location
|
|
692
|
+
);
|
|
693
|
+
return this.setType(node, freshTypeVar());
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// Handle field access on TypeVariable - constrain it to be a RecordType
|
|
697
|
+
// This enables type inference through field access patterns
|
|
698
|
+
if (objectType instanceof TypeVariable) {
|
|
699
|
+
const resolved = objectType.resolve();
|
|
700
|
+
if (resolved !== objectType) {
|
|
701
|
+
// TypeVariable was resolved - check if it's a RecordType now
|
|
702
|
+
if (resolved instanceof RecordType) {
|
|
703
|
+
const fieldType = resolved.fields.get(propName);
|
|
704
|
+
if (fieldType) {
|
|
705
|
+
return this.setType(node, fieldType);
|
|
706
|
+
}
|
|
707
|
+
// Field not found in resolved record
|
|
708
|
+
// If the record is open (from inference), extend it with the new field
|
|
709
|
+
if (resolved.open) {
|
|
710
|
+
const fieldTypeVar = freshTypeVar();
|
|
711
|
+
resolved.fields.set(propName, fieldTypeVar);
|
|
712
|
+
return this.setType(node, fieldTypeVar);
|
|
713
|
+
}
|
|
714
|
+
// Closed record - field access error
|
|
715
|
+
this.addError(
|
|
716
|
+
`Field '${propName}' does not exist on record`,
|
|
717
|
+
node.location
|
|
718
|
+
);
|
|
719
|
+
}
|
|
720
|
+
// For other resolved types, fall through
|
|
721
|
+
} else {
|
|
722
|
+
// Unresolved TypeVariable - constrain it to be a RecordType with this field
|
|
723
|
+
// Mark as open so it can be extended with additional fields during inference
|
|
724
|
+
const fieldTypeVar = freshTypeVar();
|
|
725
|
+
const fields = new Map();
|
|
726
|
+
fields.set(propName, fieldTypeVar);
|
|
727
|
+
const recordConstraint = new RecordType(fields, true); // open = true
|
|
728
|
+
unify(objectType, recordConstraint);
|
|
729
|
+
return this.setType(node, fieldTypeVar);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// If we reach here, objectType is a concrete non-record type (num, string, etc.)
|
|
734
|
+
// Property access on primitives is an error
|
|
735
|
+
if (objectType instanceof PrimitiveType) {
|
|
736
|
+
const typeStr = formatType(this.toStructuredType(objectType));
|
|
737
|
+
this.addError(
|
|
738
|
+
`Cannot access property '${propName}' on type '${typeStr}'. ` +
|
|
739
|
+
`Property access is only valid on records.`,
|
|
740
|
+
node.location
|
|
741
|
+
);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
return this.setType(node, freshTypeVar());
|
|
745
|
+
},
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* Visit a range expression
|
|
749
|
+
*/
|
|
750
|
+
visitRange(node, env) {
|
|
751
|
+
this.visit(node.start, env);
|
|
752
|
+
this.visit(node.end, env);
|
|
753
|
+
if (node.step) {
|
|
754
|
+
this.visit(node.step, env);
|
|
755
|
+
}
|
|
756
|
+
return this.setType(node, new ArrayType(INT, 1));
|
|
757
|
+
},
|
|
758
|
+
|
|
759
|
+
/**
|
|
760
|
+
* Visit a variable update (loop state transition)
|
|
761
|
+
*/
|
|
762
|
+
visitVariableUpdate(node, env) {
|
|
763
|
+
if (node.value) {
|
|
764
|
+
this.visit(node.value, env);
|
|
765
|
+
}
|
|
766
|
+
return this.setType(node, UNIT);
|
|
767
|
+
},
|
|
768
|
+
};
|