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.
Files changed (68) hide show
  1. package/README.md +52 -0
  2. package/StoneEngine.js +879 -0
  3. package/StoneEngineService.js +1727 -0
  4. package/adapters/FileSystemAdapter.js +230 -0
  5. package/adapters/OutputAdapter.js +208 -0
  6. package/adapters/index.js +6 -0
  7. package/cli/CLIOutputAdapter.js +196 -0
  8. package/cli/DaemonClient.js +349 -0
  9. package/cli/JSONOutputAdapter.js +135 -0
  10. package/cli/ReplSession.js +567 -0
  11. package/cli/ViewerServer.js +590 -0
  12. package/cli/commands/check.js +84 -0
  13. package/cli/commands/daemon.js +189 -0
  14. package/cli/commands/kill.js +66 -0
  15. package/cli/commands/package.js +713 -0
  16. package/cli/commands/ps.js +65 -0
  17. package/cli/commands/run.js +537 -0
  18. package/cli/entry.js +169 -0
  19. package/cli/index.js +14 -0
  20. package/cli/stonec.js +358 -0
  21. package/cli/test-compiler.js +181 -0
  22. package/cli/viewer/index.html +495 -0
  23. package/daemon/IPCServer.js +455 -0
  24. package/daemon/ProcessManager.js +327 -0
  25. package/daemon/ProcessRunner.js +307 -0
  26. package/daemon/daemon.js +398 -0
  27. package/daemon/index.js +16 -0
  28. package/frontend/analysis/index.js +5 -0
  29. package/frontend/analysis/livenessAnalyzer.js +568 -0
  30. package/frontend/analysis/treeShaker.js +265 -0
  31. package/frontend/index.js +20 -0
  32. package/frontend/parsing/astBuilder.js +2196 -0
  33. package/frontend/parsing/index.js +7 -0
  34. package/frontend/parsing/sonParser.js +592 -0
  35. package/frontend/parsing/stoneAstTypes.js +703 -0
  36. package/frontend/parsing/terminal-registry.js +435 -0
  37. package/frontend/parsing/tokenizer.js +692 -0
  38. package/frontend/type-checker/OverloadedFunctionType.js +43 -0
  39. package/frontend/type-checker/TypeEnvironment.js +165 -0
  40. package/frontend/type-checker/bidirectionalInference.js +149 -0
  41. package/frontend/type-checker/index.js +10 -0
  42. package/frontend/type-checker/moduleAnalysis.js +248 -0
  43. package/frontend/type-checker/operatorMappings.js +35 -0
  44. package/frontend/type-checker/overloadResolution.js +605 -0
  45. package/frontend/type-checker/typeChecker.js +452 -0
  46. package/frontend/type-checker/typeCompatibility.js +389 -0
  47. package/frontend/type-checker/visitors/controlFlow.js +483 -0
  48. package/frontend/type-checker/visitors/functions.js +604 -0
  49. package/frontend/type-checker/visitors/index.js +38 -0
  50. package/frontend/type-checker/visitors/literals.js +341 -0
  51. package/frontend/type-checker/visitors/modules.js +159 -0
  52. package/frontend/type-checker/visitors/operators.js +109 -0
  53. package/frontend/type-checker/visitors/statements.js +768 -0
  54. package/frontend/types/index.js +5 -0
  55. package/frontend/types/operatorMap.js +134 -0
  56. package/frontend/types/types.js +2046 -0
  57. package/frontend/utils/errorCollector.js +244 -0
  58. package/frontend/utils/index.js +5 -0
  59. package/frontend/utils/moduleResolver.js +479 -0
  60. package/package.json +50 -0
  61. package/packages/browserCache.js +359 -0
  62. package/packages/fetcher.js +236 -0
  63. package/packages/index.js +130 -0
  64. package/packages/lockfile.js +271 -0
  65. package/packages/manifest.js +291 -0
  66. package/packages/packageResolver.js +356 -0
  67. package/packages/resolver.js +310 -0
  68. 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
+ };