tova 0.2.9 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,6 +8,7 @@ import {
8
8
  typeAnnotationToType, typeFromString, typesCompatible,
9
9
  isNumericType, isFloatNarrowing,
10
10
  } from './types.js';
11
+ import { ErrorCode, WarningCode } from '../diagnostics/error-codes.js';
11
12
 
12
13
  const _JS_GLOBALS = new Set([
13
14
  'console', 'document', 'window', 'globalThis', 'self',
@@ -29,6 +30,22 @@ const _JS_GLOBALS = new Set([
29
30
  'Buffer', 'atob', 'btoa',
30
31
  ]);
31
32
 
33
+ function levenshtein(a, b) {
34
+ if (a.length === 0) return b.length;
35
+ if (b.length === 0) return a.length;
36
+ const m = [];
37
+ for (let i = 0; i <= b.length; i++) m[i] = [i];
38
+ for (let j = 0; j <= a.length; j++) m[0][j] = j;
39
+ for (let i = 1; i <= b.length; i++) {
40
+ for (let j = 1; j <= a.length; j++) {
41
+ m[i][j] = b[i-1] === a[j-1]
42
+ ? m[i-1][j-1]
43
+ : Math.min(m[i-1][j-1] + 1, m[i][j-1] + 1, m[i-1][j] + 1);
44
+ }
45
+ }
46
+ return m[b.length][a.length];
47
+ }
48
+
32
49
  const _TOVA_RUNTIME = new Set([
33
50
  'Ok', 'Err', 'Some', 'None', 'Result', 'Option',
34
51
  'db', 'server', 'client', 'shared',
@@ -48,6 +65,9 @@ export class Analyzer {
48
65
  this._functionReturnTypeStack = []; // Stack of expected return types for type checking
49
66
  this._asyncDepth = 0; // Track nesting inside async functions for await validation
50
67
 
68
+ // Propagate strict mode to the type system
69
+ Type.strictMode = this.strict;
70
+
51
71
  // Type registry for LSP
52
72
  this.typeRegistry = {
53
73
  types: new Map(), // type name → ADTType | RecordType
@@ -125,64 +145,134 @@ export class Analyzer {
125
145
  'hypot', 'lerp', 'divmod', 'avg',
126
146
  // Date/Time
127
147
  'now', 'now_iso',
148
+ // Scripting
149
+ 'env', 'set_env', 'args', 'exit',
150
+ 'exists', 'is_dir', 'is_file', 'ls', 'glob_files',
151
+ 'mkdir', 'rm', 'cp', 'mv', 'cwd', 'chdir',
152
+ 'read_text', 'read_bytes', 'write_text',
153
+ 'sh', 'exec',
154
+ // Scripting: new
155
+ 'read_stdin', 'read_lines',
156
+ 'script_path', 'script_dir',
157
+ 'parse_args',
158
+ 'color', 'bold', 'dim',
159
+ // Scripting: signals, file stat, path utils, symlinks, async shell
160
+ 'on_signal',
161
+ 'file_stat', 'file_size',
162
+ 'path_join', 'path_dirname', 'path_basename', 'path_resolve', 'path_ext', 'path_relative',
163
+ 'symlink', 'readlink', 'is_symlink',
164
+ 'spawn',
165
+ // Namespace modules
166
+ 'math', 'str', 'arr', 'dt', 're', 'json', 'fs', 'url',
167
+ // Advanced collections
168
+ 'OrderedDict', 'DefaultDict', 'Counter', 'Deque', 'collections',
128
169
  ];
129
170
  for (const name of builtins) {
130
171
  this.globalScope.define(name, new Symbol(name, 'builtin', null, false, { line: 0, column: 0, file: '<builtin>' }));
131
172
  }
132
173
  }
133
174
 
134
- error(message, loc) {
175
+ error(message, loc, hint = null, opts = {}) {
135
176
  const l = loc || { line: 0, column: 0, file: this.filename };
136
- this.errors.push({
177
+ const e = {
137
178
  message,
138
179
  file: l.file || this.filename,
139
180
  line: l.line,
140
181
  column: l.column,
141
- });
182
+ };
183
+ if (hint) e.hint = hint;
184
+ if (opts.code) e.code = opts.code;
185
+ if (opts.length) e.length = opts.length;
186
+ if (opts.fix) e.fix = opts.fix;
187
+ this.errors.push(e);
142
188
  }
143
189
 
144
- warn(message, loc) {
190
+ warn(message, loc, hint = null, opts = {}) {
145
191
  const l = loc || { line: 0, column: 0, file: this.filename };
146
- this.warnings.push({
192
+ const w = {
147
193
  message,
148
194
  file: l.file || this.filename,
149
195
  line: l.line,
150
196
  column: l.column,
151
- });
197
+ };
198
+ if (hint) w.hint = hint;
199
+ if (opts.code) w.code = opts.code;
200
+ if (opts.length) w.length = opts.length;
201
+ if (opts.fix) w.fix = opts.fix;
202
+ this.warnings.push(w);
152
203
  }
153
204
 
154
- strictError(message, loc) {
205
+ strictError(message, loc, hint = null, opts = {}) {
155
206
  if (this.strict) {
156
- this.error(message, loc);
207
+ this.error(message, loc, hint, opts);
157
208
  } else {
158
- this.warn(message, loc);
209
+ this.warn(message, loc, hint, opts);
159
210
  }
160
211
  }
161
212
 
162
- analyze() {
163
- // Pre-pass: collect named server block functions for inter-server RPC validation
164
- this.serverBlockFunctions = new Map(); // blockName -> [functionName, ...]
165
- const collectFns = (stmts) => {
166
- const fns = [];
167
- for (const stmt of stmts) {
168
- if (stmt.type === 'FunctionDeclaration') {
169
- fns.push(stmt.name);
170
- } else if (stmt.type === 'RouteGroupDeclaration') {
171
- fns.push(...collectFns(stmt.body));
172
- }
213
+ // ─── Naming convention helpers ─────────────────────────────
214
+
215
+ _isSnakeCase(name) {
216
+ if (name.startsWith('_')) return true;
217
+ if (name.length === 1) return true;
218
+ if (/^[A-Z][A-Z0-9]*(_[A-Z0-9]+)*$/.test(name)) return true; // UPPER_SNAKE_CASE
219
+ return /^[a-z][a-z0-9]*(_[a-z0-9]+)*$/.test(name);
220
+ }
221
+
222
+ _isPascalCase(name) {
223
+ return /^[A-Z][a-zA-Z0-9]*$/.test(name);
224
+ }
225
+
226
+ _isUpperSnakeCase(name) {
227
+ return /^[A-Z][A-Z0-9]*(_[A-Z0-9]+)*$/.test(name);
228
+ }
229
+
230
+ _toSnakeCase(name) {
231
+ return name.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toLowerCase();
232
+ }
233
+
234
+ _toPascalCase(name) {
235
+ return name.replace(/(^|_)([a-z])/g, (_, __, c) => c.toUpperCase());
236
+ }
237
+
238
+ _checkNamingConvention(name, kind, loc) {
239
+ if (!name || name.startsWith('_') || name.length === 1) return;
240
+ if (this._isUpperSnakeCase(name)) return; // constants are valid for variables
241
+
242
+ if (kind === 'type' || kind === 'component' || kind === 'store') {
243
+ if (!this._isPascalCase(name)) {
244
+ const suggested = this._toPascalCase(name);
245
+ this.warn(
246
+ `${kind[0].toUpperCase() + kind.slice(1)} '${name}' should use PascalCase`,
247
+ loc,
248
+ `Rename '${name}' to '${suggested}'`,
249
+ { code: 'W100', length: name.length, fix: { description: `Rename to '${suggested}'`, replacement: suggested } }
250
+ );
173
251
  }
174
- return fns;
175
- };
176
- for (const node of this.ast.body) {
177
- if (node.type === 'ServerBlock' && node.name) {
178
- const fns = collectFns(node.body);
179
- if (this.serverBlockFunctions.has(node.name)) {
180
- this.serverBlockFunctions.get(node.name).push(...fns);
181
- } else {
182
- this.serverBlockFunctions.set(node.name, fns);
183
- }
252
+ } else {
253
+ // function, variable, parameter
254
+ if (!this._isSnakeCase(name)) {
255
+ const suggested = this._toSnakeCase(name);
256
+ this.warn(
257
+ `${kind[0].toUpperCase() + kind.slice(1)} '${name}' should use snake_case`,
258
+ loc,
259
+ `Rename '${name}' to '${suggested}'`,
260
+ { code: 'W100', length: name.length, fix: { description: `Rename to '${suggested}'`, replacement: suggested } }
261
+ );
184
262
  }
185
263
  }
264
+ }
265
+
266
+ analyze() {
267
+ // Pre-pass: collect named server block functions for inter-server RPC validation
268
+ const hasServerBlocks = this.ast.body.some(n => n.type === 'ServerBlock');
269
+ if (hasServerBlocks) {
270
+ const { collectServerBlockFunctions, installServerAnalyzer } = import.meta.require('./server-analyzer.js');
271
+ installServerAnalyzer(Analyzer);
272
+ this.serverBlockFunctions = collectServerBlockFunctions(this.ast);
273
+ } else {
274
+ this.serverBlockFunctions = new Map();
275
+ }
186
276
 
187
277
  this.visitProgram(this.ast);
188
278
 
@@ -216,7 +306,34 @@ export class Analyzer {
216
306
  if (sym.kind === 'parameter') continue;
217
307
 
218
308
  if (!sym.used && sym.loc && sym.loc.line > 0) {
219
- this.warn(`'${name}' is declared but never used`, sym.loc);
309
+ this.warn(`'${name}' is declared but never used`, sym.loc, "prefix with _ to suppress", {
310
+ code: 'W001',
311
+ length: name.length,
312
+ fix: { description: `Prefix with _ to suppress: _${name}`, replacement: `_${name}` },
313
+ });
314
+ }
315
+ }
316
+ }
317
+
318
+ // Check unused functions at module/server/client/shared level
319
+ for (const scope of this._allScopes) {
320
+ if (scope.context !== 'module' && scope.context !== 'server' &&
321
+ scope.context !== 'client' && scope.context !== 'shared') continue;
322
+
323
+ for (const [name, sym] of scope.symbols) {
324
+ if (sym.kind !== 'function') continue;
325
+ if (name.startsWith('_')) continue;
326
+ if (sym.isPublic) continue;
327
+ if (sym.extern) continue;
328
+ if (sym._variantOf) continue; // ADT variant constructors
329
+ if (name === 'main') continue;
330
+
331
+ if (!sym.used && sym.loc && sym.loc.line > 0) {
332
+ this.warn(`Function '${name}' is declared but never used`, sym.loc, "prefix with _ to suppress", {
333
+ code: 'W002',
334
+ length: name.length,
335
+ fix: { description: `Prefix with _ to suppress: _${name}`, replacement: `_${name}` },
336
+ });
220
337
  }
221
338
  }
222
339
  }
@@ -281,7 +398,27 @@ export class Analyzer {
281
398
  const fnSym = this.currentScope.lookup(name);
282
399
  if (fnSym && fnSym.kind === 'function') {
283
400
  if (fnSym._variantOf) return fnSym._variantOf;
284
- if (fnSym.type) return this._typeAnnotationToString(fnSym.type);
401
+ if (fnSym.type) {
402
+ let retType = this._typeAnnotationToString(fnSym.type);
403
+ // For generic functions, infer type params from call arguments
404
+ if (fnSym._typeParams && fnSym._typeParams.length > 0 && fnSym._paramTypes) {
405
+ const typeParamBindings = new Map();
406
+ for (let i = 0; i < expr.arguments.length && i < fnSym._paramTypes.length; i++) {
407
+ const arg = expr.arguments[i];
408
+ if (arg.type === 'NamedArgument' || arg.type === 'SpreadExpression') continue;
409
+ const paramTypeAnn = fnSym._paramTypes[i];
410
+ if (!paramTypeAnn) continue;
411
+ const actualType = this._inferType(arg);
412
+ if (actualType) {
413
+ this._inferTypeParamBindings(paramTypeAnn, actualType, fnSym._typeParams, typeParamBindings);
414
+ }
415
+ }
416
+ if (typeParamBindings.size > 0) {
417
+ retType = this._substituteTypeParams(retType, typeParamBindings);
418
+ }
419
+ }
420
+ return retType;
421
+ }
285
422
  }
286
423
  }
287
424
  return null;
@@ -298,7 +435,9 @@ export class Analyzer {
298
435
  if (['+', '-', '*', '/', '%', '**'].includes(expr.operator)) {
299
436
  const lt = this._inferType(expr.left);
300
437
  const rt = this._inferType(expr.right);
438
+ if (!lt && !rt) return null;
301
439
  if (lt === 'Float' || rt === 'Float') return 'Float';
440
+ if (lt === 'String' || rt === 'String') return 'String';
302
441
  return 'Int';
303
442
  }
304
443
  if (['==', '!=', '<', '>', '<=', '>='].includes(expr.operator)) return 'Bool';
@@ -309,11 +448,96 @@ export class Analyzer {
309
448
  return null;
310
449
  case 'LogicalExpression':
311
450
  return 'Bool';
451
+ case 'PipeExpression':
452
+ return this._inferPipeType(expr);
453
+ case 'MemberExpression':
454
+ // Infer .length as Int
455
+ if (expr.property === 'length') return 'Int';
456
+ return null;
312
457
  default:
313
458
  return null;
314
459
  }
315
460
  }
316
461
 
462
+ /**
463
+ * Infer the result type of a pipe expression like `arr |> filter(fn(x) x > 0) |> map(fn(x) x * 2)`.
464
+ * The left side provides the input type; the right side determines the output type.
465
+ */
466
+ _inferPipeType(expr) {
467
+ const inputType = this._inferType(expr.left);
468
+ const right = expr.right;
469
+
470
+ if (right.type === 'CallExpression' && right.callee.type === 'Identifier') {
471
+ const fnName = right.callee.name;
472
+
473
+ // Collection operations that preserve the array type
474
+ if (['filter', 'sorted', 'reversed', 'unique', 'take', 'drop', 'skip'].includes(fnName)) {
475
+ return inputType; // Same type as input
476
+ }
477
+
478
+ // map: transforms element type based on the mapper function
479
+ if (fnName === 'map') {
480
+ if (inputType && inputType.startsWith('[') && inputType.endsWith(']')) {
481
+ // Try to infer the return type from the mapper function
482
+ const mapperArg = right.arguments.length > 0 ? right.arguments[0] : null;
483
+ if (mapperArg) {
484
+ const mapperRetType = this._inferLambdaReturnType(mapperArg, inputType.slice(1, -1));
485
+ if (mapperRetType) return `[${mapperRetType}]`;
486
+ }
487
+ }
488
+ return inputType; // Fallback: preserve input type
489
+ }
490
+
491
+ // flat_map / flatten: reduce nesting
492
+ if (fnName === 'flat_map' || fnName === 'flatMap') {
493
+ if (inputType && inputType.startsWith('[') && inputType.endsWith(']')) {
494
+ return inputType; // Simplified: same element type
495
+ }
496
+ }
497
+ if (fnName === 'flatten') {
498
+ if (inputType && inputType.startsWith('[[') && inputType.endsWith(']]')) {
499
+ return inputType.slice(1, -1); // [[T]] -> [T]
500
+ }
501
+ return inputType;
502
+ }
503
+
504
+ // Reduction operations
505
+ if (fnName === 'reduce' || fnName === 'fold') return null; // Can't easily infer
506
+ if (fnName === 'join') return 'String';
507
+ if (fnName === 'count' || fnName === 'len' || fnName === 'length') return 'Int';
508
+ if (fnName === 'sum') return inputType === '[Float]' ? 'Float' : 'Int';
509
+ if (fnName === 'any' || fnName === 'all' || fnName === 'every' || fnName === 'some') return 'Bool';
510
+ if (fnName === 'first' || fnName === 'last' || fnName === 'find') {
511
+ if (inputType && inputType.startsWith('[') && inputType.endsWith(']')) {
512
+ return inputType.slice(1, -1); // [T] -> T
513
+ }
514
+ }
515
+
516
+ // For user-defined functions, fall back to checking their return type
517
+ const fnSym = this.currentScope.lookup(fnName);
518
+ if (fnSym && fnSym.kind === 'function' && fnSym.type) {
519
+ return this._typeAnnotationToString(fnSym.type);
520
+ }
521
+ }
522
+
523
+ // If we can't infer the right side, return null
524
+ return null;
525
+ }
526
+
527
+ /**
528
+ * Try to infer the return type of a lambda expression given the input element type.
529
+ */
530
+ _inferLambdaReturnType(lambdaExpr, inputElementType) {
531
+ if (!lambdaExpr) return null;
532
+ if (lambdaExpr.type === 'LambdaExpression') {
533
+ // For simple expression bodies, infer the result type
534
+ if (lambdaExpr.body && lambdaExpr.body.type !== 'BlockStatement') {
535
+ return this._inferType(lambdaExpr.body);
536
+ }
537
+ }
538
+ return null;
539
+ }
540
+
317
541
  _typeAnnotationToString(ann) {
318
542
  if (!ann) return null;
319
543
  if (typeof ann === 'string') return ann;
@@ -330,6 +554,8 @@ export class Analyzer {
330
554
  return `(${ann.elementTypes.map(t => this._typeAnnotationToString(t) || 'Any').join(', ')})`;
331
555
  case 'FunctionTypeAnnotation':
332
556
  return 'Function';
557
+ case 'UnionTypeAnnotation':
558
+ return ann.members.map(m => this._typeAnnotationToString(m) || 'Any').join(' | ');
333
559
  default:
334
560
  return null;
335
561
  }
@@ -362,14 +588,29 @@ export class Analyzer {
362
588
  if (!expected || !actual) return true;
363
589
  if (expected === 'Any' || actual === 'Any') return true;
364
590
  if (expected === '_' || actual === '_') return true;
591
+ // Resolve type aliases before comparison
592
+ expected = this._resolveTypeAlias(expected);
593
+ actual = this._resolveTypeAlias(actual);
365
594
  // Exact match
366
595
  if (expected === actual) return true;
367
- // Numeric compatibility: Int and Float are interchangeable
368
- const numerics = new Set(['Int', 'Float']);
369
- if (numerics.has(expected) && numerics.has(actual)) return true;
596
+ // Numeric compatibility: Int -> Float widening is safe; Float -> Int requires explicit conversion
597
+ if (expected === 'Float' && actual === 'Int') return true;
598
+ if (expected === 'Int' && actual === 'Float') return false; // caller should emit warning/error
370
599
  // Nil is compatible with Option
371
600
  if (actual === 'Nil' && (expected === 'Option' || expected.startsWith('Option'))) return true;
372
601
  if ((expected === 'Nil') && (actual === 'Option' || actual.startsWith('Option'))) return true;
602
+ // Union type compatibility: actual must be assignable to one of the expected union members
603
+ if (expected.includes(' | ')) {
604
+ const members = expected.split(' | ').map(m => m.trim());
605
+ return members.some(m => this._typesCompatible(m, actual));
606
+ }
607
+ // If actual is a union, every member must be compatible with expected
608
+ if (actual.includes(' | ')) {
609
+ const members = actual.split(' | ').map(m => m.trim());
610
+ return members.every(m => this._typesCompatible(expected, m));
611
+ }
612
+ // Nil is compatible with union types that include Nil
613
+ if (actual === 'Nil' && expected.includes('Nil')) return true;
373
614
  // Array compatibility: check element types
374
615
  if (expected.startsWith('[') && actual.startsWith('[')) {
375
616
  const expEl = expected.slice(1, -1);
@@ -414,8 +655,41 @@ export class Analyzer {
414
655
  if (!node) return;
415
656
 
416
657
  switch (node.type) {
417
- case 'ServerBlock': return this.visitServerBlock(node);
418
- case 'ClientBlock': return this.visitClientBlock(node);
658
+ case 'ServerBlock':
659
+ case 'RouteDeclaration':
660
+ case 'MiddlewareDeclaration':
661
+ case 'HealthCheckDeclaration':
662
+ case 'CorsDeclaration':
663
+ case 'ErrorHandlerDeclaration':
664
+ case 'WebSocketDeclaration':
665
+ case 'StaticDeclaration':
666
+ case 'DiscoverDeclaration':
667
+ case 'AuthDeclaration':
668
+ case 'MaxBodyDeclaration':
669
+ case 'RouteGroupDeclaration':
670
+ case 'RateLimitDeclaration':
671
+ case 'LifecycleHookDeclaration':
672
+ case 'SubscribeDeclaration':
673
+ case 'EnvDeclaration':
674
+ case 'ScheduleDeclaration':
675
+ case 'UploadDeclaration':
676
+ case 'SessionDeclaration':
677
+ case 'DbDeclaration':
678
+ case 'TlsDeclaration':
679
+ case 'CompressionDeclaration':
680
+ case 'BackgroundJobDeclaration':
681
+ case 'CacheDeclaration':
682
+ case 'SseDeclaration':
683
+ case 'ModelDeclaration':
684
+ return this._visitServerNode(node);
685
+ case 'AiConfigDeclaration': return; // handled at block level
686
+ case 'ClientBlock':
687
+ case 'StateDeclaration':
688
+ case 'ComputedDeclaration':
689
+ case 'EffectDeclaration':
690
+ case 'ComponentDeclaration':
691
+ case 'StoreDeclaration':
692
+ return this._visitClientNode(node);
419
693
  case 'SharedBlock': return this.visitSharedBlock(node);
420
694
  case 'Assignment': return this.visitAssignment(node);
421
695
  case 'VarDeclaration': return this.visitVarDeclaration(node);
@@ -428,6 +702,7 @@ export class Analyzer {
428
702
  case 'IfStatement': return this.visitIfStatement(node);
429
703
  case 'ForStatement': return this.visitForStatement(node);
430
704
  case 'WhileStatement': return this.visitWhileStatement(node);
705
+ case 'LoopStatement': return this.visitLoopStatement(node);
431
706
  case 'TryCatchStatement': return this.visitTryCatchStatement(node);
432
707
  case 'ReturnStatement': return this.visitReturnStatement(node);
433
708
  case 'ExpressionStatement': return this.visitExpression(node.expression);
@@ -437,37 +712,6 @@ export class Analyzer {
437
712
  case 'ContinueStatement': return this.visitContinueStatement(node);
438
713
  case 'GuardStatement': return this.visitGuardStatement(node);
439
714
  case 'InterfaceDeclaration': return this.visitInterfaceDeclaration(node);
440
- case 'StateDeclaration': return this.visitStateDeclaration(node);
441
- case 'ComputedDeclaration': return this.visitComputedDeclaration(node);
442
- case 'EffectDeclaration': return this.visitEffectDeclaration(node);
443
- case 'ComponentDeclaration': return this.visitComponentDeclaration(node);
444
- case 'StoreDeclaration': return this.visitStoreDeclaration(node);
445
- case 'RouteDeclaration': return this.visitRouteDeclaration(node);
446
- case 'MiddlewareDeclaration': return this.visitMiddlewareDeclaration(node);
447
- case 'HealthCheckDeclaration': return this.visitHealthCheckDeclaration(node);
448
- case 'CorsDeclaration': return this.visitCorsDeclaration(node);
449
- case 'ErrorHandlerDeclaration': return this.visitErrorHandlerDeclaration(node);
450
- case 'WebSocketDeclaration': return this.visitWebSocketDeclaration(node);
451
- case 'StaticDeclaration': return this.visitStaticDeclaration(node);
452
- case 'DiscoverDeclaration': return this.visitDiscoverDeclaration(node);
453
- case 'AuthDeclaration': return this.visitAuthDeclaration(node);
454
- case 'MaxBodyDeclaration': return this.visitMaxBodyDeclaration(node);
455
- case 'RouteGroupDeclaration': return this.visitRouteGroupDeclaration(node);
456
- case 'RateLimitDeclaration': return this.visitRateLimitDeclaration(node);
457
- case 'LifecycleHookDeclaration': return this.visitLifecycleHookDeclaration(node);
458
- case 'SubscribeDeclaration': return this.visitSubscribeDeclaration(node);
459
- case 'EnvDeclaration': return this.visitEnvDeclaration(node);
460
- case 'ScheduleDeclaration': return this.visitScheduleDeclaration(node);
461
- case 'UploadDeclaration': return this.visitUploadDeclaration(node);
462
- case 'SessionDeclaration': return this.visitSessionDeclaration(node);
463
- case 'DbDeclaration': return this.visitDbDeclaration(node);
464
- case 'TlsDeclaration': return this.visitTlsDeclaration(node);
465
- case 'CompressionDeclaration': return this.visitCompressionDeclaration(node);
466
- case 'BackgroundJobDeclaration': return this.visitBackgroundJobDeclaration(node);
467
- case 'CacheDeclaration': return this.visitCacheDeclaration(node);
468
- case 'SseDeclaration': return this.visitSseDeclaration(node);
469
- case 'ModelDeclaration': return this.visitModelDeclaration(node);
470
- case 'AiConfigDeclaration': return; // handled at block level
471
715
  case 'DataBlock': return this.visitDataBlock(node);
472
716
  case 'SourceDeclaration': return;
473
717
  case 'PipelineDeclaration': return;
@@ -475,6 +719,7 @@ export class Analyzer {
475
719
  case 'RefreshPolicy': return;
476
720
  case 'RefinementType': return;
477
721
  case 'TestBlock': return this.visitTestBlock(node);
722
+ case 'BenchBlock': return this.visitTestBlock(node);
478
723
  case 'ComponentStyleBlock': return; // raw CSS — no analysis needed
479
724
  case 'ImplDeclaration': return this.visitImplDeclaration(node);
480
725
  case 'TraitDeclaration': return this.visitTraitDeclaration(node);
@@ -487,6 +732,24 @@ export class Analyzer {
487
732
  }
488
733
  }
489
734
 
735
+ _visitServerNode(node) {
736
+ if (!Analyzer.prototype._serverAnalyzerInstalled) {
737
+ const { installServerAnalyzer } = import.meta.require('./server-analyzer.js');
738
+ installServerAnalyzer(Analyzer);
739
+ }
740
+ const methodName = 'visit' + node.type;
741
+ return this[methodName](node);
742
+ }
743
+
744
+ _visitClientNode(node) {
745
+ if (!Analyzer.prototype._clientAnalyzerInstalled) {
746
+ const { installClientAnalyzer } = import.meta.require('./client-analyzer.js');
747
+ installClientAnalyzer(Analyzer);
748
+ }
749
+ const methodName = 'visit' + node.type;
750
+ return this[methodName](node);
751
+ }
752
+
490
753
  visitExpression(node) {
491
754
  if (!node) return;
492
755
 
@@ -567,8 +830,16 @@ export class Analyzer {
567
830
  return;
568
831
  case 'ObjectLiteral':
569
832
  for (const prop of node.properties) {
570
- this.visitExpression(prop.key);
571
- this.visitExpression(prop.value);
833
+ if (prop.spread) {
834
+ // Spread property: {...expr}
835
+ this.visitExpression(prop.argument);
836
+ } else if (prop.shorthand) {
837
+ // Shorthand: {name} — key IS the variable reference
838
+ this.visitExpression(prop.key);
839
+ } else {
840
+ // Non-shorthand: {key: value} — only visit value, key is a label
841
+ this.visitExpression(prop.value);
842
+ }
572
843
  }
573
844
  return;
574
845
  case 'ListComprehension':
@@ -593,7 +864,7 @@ export class Analyzer {
593
864
  return;
594
865
  case 'AwaitExpression':
595
866
  if (this._asyncDepth === 0) {
596
- this.error("'await' can only be used inside an async function", node.loc);
867
+ this.error("'await' can only be used inside an async function", node.loc, "add 'async' to the enclosing function declaration", { code: 'E300' });
597
868
  }
598
869
  this.visitExpression(node.argument);
599
870
  return;
@@ -613,7 +884,8 @@ export class Analyzer {
613
884
  this.visitNode(node.elseBody);
614
885
  return;
615
886
  case 'JSXElement':
616
- return this.visitJSXElement(node);
887
+ case 'JSXFragment':
888
+ return this._visitClientNode(node);
617
889
  // Column expressions (for table operations) — no semantic analysis needed
618
890
  case 'ColumnExpression':
619
891
  return;
@@ -627,49 +899,6 @@ export class Analyzer {
627
899
 
628
900
  // ─── Block visitors ───────────────────────────────────────
629
901
 
630
- visitServerBlock(node) {
631
- const prevScope = this.currentScope;
632
- const prevServerBlockName = this._currentServerBlockName;
633
- this._currentServerBlockName = node.name || null;
634
- this.currentScope = this.currentScope.child('server');
635
-
636
- try {
637
- // Register peer server block names as valid identifiers in this scope
638
- if (node.name && this.serverBlockFunctions.size > 0) {
639
- for (const [peerName] of this.serverBlockFunctions) {
640
- if (peerName !== node.name) {
641
- try {
642
- this.currentScope.define(peerName,
643
- new Symbol(peerName, 'builtin', null, false, { line: 0, column: 0, file: '<peer-server>' }));
644
- } catch (e) {
645
- // Ignore if already defined
646
- }
647
- }
648
- }
649
- }
650
-
651
- // Register AI provider names as variables (named: claude, gpt, etc.; default: ai)
652
- for (const stmt of node.body) {
653
- if (stmt.type === 'AiConfigDeclaration') {
654
- const aiName = stmt.name || 'ai';
655
- try {
656
- this.currentScope.define(aiName,
657
- new Symbol(aiName, 'builtin', null, false, stmt.loc));
658
- } catch (e) {
659
- // Ignore if already defined
660
- }
661
- }
662
- }
663
-
664
- for (const stmt of node.body) {
665
- this.visitNode(stmt);
666
- }
667
- } finally {
668
- this.currentScope = prevScope;
669
- this._currentServerBlockName = prevServerBlockName;
670
- }
671
- }
672
-
673
902
  visitDataBlock(node) {
674
903
  // Register source and pipeline names in global scope
675
904
  for (const stmt of node.body) {
@@ -689,17 +918,7 @@ export class Analyzer {
689
918
  }
690
919
  }
691
920
 
692
- visitClientBlock(node) {
693
- const prevScope = this.currentScope;
694
- this.currentScope = this.currentScope.child('client');
695
- try {
696
- for (const stmt of node.body) {
697
- this.visitNode(stmt);
698
- }
699
- } finally {
700
- this.currentScope = prevScope;
701
- }
702
- }
921
+ // visitClientBlock and other client visitors are in client-analyzer.js (lazy-loaded)
703
922
 
704
923
  visitSharedBlock(node) {
705
924
  const prevScope = this.currentScope;
@@ -712,10 +931,10 @@ export class Analyzer {
712
931
  } finally {
713
932
  this.currentScope = prevScope;
714
933
  }
715
- // Promote shared symbols (types, functions) to parent scope
716
- // so server/client blocks can reference them
934
+ // Promote shared types and functions to parent scope
935
+ // so server/client blocks can reference them (but not variables)
717
936
  for (const [name, sym] of sharedScope.symbols) {
718
- if (!prevScope.symbols.has(name)) {
937
+ if (!prevScope.symbols.has(name) && (sym.kind === 'type' || sym.kind === 'function')) {
719
938
  prevScope.symbols.set(name, sym);
720
939
  }
721
940
  }
@@ -735,17 +954,21 @@ export class Analyzer {
735
954
  const existing = this._lookupAssignTarget(target);
736
955
  if (existing) {
737
956
  if (!existing.mutable) {
738
- this.error(`Cannot reassign immutable variable '${target}'. Use 'var' for mutable variables.`, node.loc);
957
+ this.error(`Cannot reassign immutable variable '${target}'. Use 'var' for mutable variables.`, node.loc, null, {
958
+ code: 'E202',
959
+ length: target.length,
960
+ fix: { description: `Change to 'var ${target}' at the original declaration to make it mutable` },
961
+ });
739
962
  }
740
963
  // Type check reassignment
741
964
  if (existing.inferredType && i < node.values.length) {
742
965
  const newType = this._inferType(node.values[i]);
743
966
  if (!this._typesCompatible(existing.inferredType, newType)) {
744
- this.strictError(`Type mismatch: '${target}' is ${existing.inferredType}, but assigned ${newType}`, node.loc);
967
+ this.strictError(`Type mismatch: '${target}' is ${existing.inferredType}, but assigned ${newType}`, node.loc, this._conversionHint(existing.inferredType, newType), { code: 'E102' });
745
968
  }
746
969
  // Float narrowing warning in strict mode
747
970
  if (this.strict && newType === 'Float' && existing.inferredType === 'Int') {
748
- this.warn(`Potential data loss: assigning Float to Int variable '${target}'`, node.loc);
971
+ this.warn(`Potential data loss: assigning Float to Int variable '${target}'`, node.loc, "use floor() or round() for explicit conversion", { code: 'W204' });
749
972
  }
750
973
  }
751
974
  existing.used = true;
@@ -754,7 +977,7 @@ export class Analyzer {
754
977
  const inferredType = i < node.values.length ? this._inferType(node.values[i]) : null;
755
978
  // Warn if this shadows a variable from an outer function scope
756
979
  if (this._existsInOuterScope(target)) {
757
- this.warn(`Variable '${target}' shadows a binding in an outer scope`, node.loc);
980
+ this.warn(`Variable '${target}' shadows a binding in an outer scope`, node.loc, null, { code: 'W101', length: target.length });
758
981
  }
759
982
  try {
760
983
  const sym = new Symbol(target, 'variable', null, false, node.loc);
@@ -763,6 +986,7 @@ export class Analyzer {
763
986
  } catch (e) {
764
987
  this.error(e.message);
765
988
  }
989
+ this._checkNamingConvention(target, 'variable', node.loc);
766
990
  }
767
991
  }
768
992
  }
@@ -782,6 +1006,7 @@ export class Analyzer {
782
1006
  } catch (e) {
783
1007
  this.error(e.message);
784
1008
  }
1009
+ this._checkNamingConvention(target, 'variable', node.loc);
785
1010
  }
786
1011
  }
787
1012
 
@@ -818,11 +1043,16 @@ export class Analyzer {
818
1043
  sym._totalParamCount = node.params.length;
819
1044
  sym._requiredParamCount = node.params.filter(p => !p.defaultValue).length;
820
1045
  sym._paramTypes = node.params.map(p => p.typeAnnotation || null);
1046
+ sym._typeParams = node.typeParams || [];
1047
+ sym.isPublic = node.isPublic || false;
821
1048
  this.currentScope.define(node.name, sym);
822
1049
  } catch (e) {
823
1050
  this.error(e.message);
824
1051
  }
825
1052
 
1053
+ // Naming convention check (skip variant constructors — handled in visitTypeDeclaration)
1054
+ this._checkNamingConvention(node.name, 'function', node.loc);
1055
+
826
1056
  const prevScope = this.currentScope;
827
1057
  this.currentScope = this.currentScope.child('function');
828
1058
  if (node.loc) {
@@ -832,7 +1062,12 @@ export class Analyzer {
832
1062
  // Push expected return type for return-statement checking
833
1063
  const expectedReturn = node.returnType ? this._typeAnnotationToString(node.returnType) : null;
834
1064
  this._functionReturnTypeStack.push(expectedReturn);
835
- if (node.isAsync) this._asyncDepth++;
1065
+ const prevAsyncDepth = this._asyncDepth;
1066
+ if (node.isAsync) {
1067
+ this._asyncDepth++;
1068
+ } else {
1069
+ this._asyncDepth = 0; // Non-async function resets async context
1070
+ }
836
1071
 
837
1072
  try {
838
1073
  for (const param of node.params) {
@@ -846,6 +1081,7 @@ export class Analyzer {
846
1081
  } catch (e) {
847
1082
  this.error(e.message);
848
1083
  }
1084
+ this._checkNamingConvention(param.name, 'parameter', param.loc);
849
1085
  }
850
1086
  if (param.defaultValue) {
851
1087
  this.visitExpression(param.defaultValue);
@@ -857,11 +1093,11 @@ export class Analyzer {
857
1093
  // Return path analysis: check that all paths return a value
858
1094
  if (expectedReturn && node.body.type === 'BlockStatement') {
859
1095
  if (!this._definitelyReturns(node.body)) {
860
- this.warn(`Function '${node.name}' declares return type ${expectedReturn} but not all code paths return a value`, node.loc);
1096
+ this.warn(`Function '${node.name}' declares return type ${expectedReturn} but not all code paths return a value`, node.loc, null, { code: 'W205' });
861
1097
  }
862
1098
  }
863
1099
  } finally {
864
- if (node.isAsync) this._asyncDepth--;
1100
+ this._asyncDepth = prevAsyncDepth;
865
1101
  this._functionReturnTypeStack.pop();
866
1102
  this.currentScope = prevScope;
867
1103
  }
@@ -913,6 +1149,8 @@ export class Analyzer {
913
1149
  }
914
1150
 
915
1151
  visitTypeDeclaration(node) {
1152
+ this._checkNamingConvention(node.name, 'type', node.loc);
1153
+
916
1154
  // Build ADT type structure
917
1155
  const variants = new Map();
918
1156
  for (const variant of node.variants) {
@@ -955,6 +1193,19 @@ export class Analyzer {
955
1193
  }
956
1194
  }
957
1195
  }
1196
+
1197
+ // Validate derive traits
1198
+ if (node.derive) {
1199
+ const builtinTraits = new Set(['Eq', 'Show', 'JSON']);
1200
+ for (const trait of node.derive) {
1201
+ if (!builtinTraits.has(trait)) {
1202
+ const traitSym = this.currentScope.lookup(trait);
1203
+ if (!traitSym || !traitSym._interfaceMethods) {
1204
+ this.warn(`Unknown trait '${trait}' in derive clause`, node.loc, null, { code: 'W303' });
1205
+ }
1206
+ }
1207
+ }
1208
+ }
958
1209
  }
959
1210
 
960
1211
  visitImportDeclaration(node) {
@@ -995,8 +1246,16 @@ export class Analyzer {
995
1246
  this.currentScope.startLoc = { line: node.loc.line, column: node.loc.column };
996
1247
  }
997
1248
  try {
1249
+ let terminated = false;
998
1250
  for (const stmt of node.body) {
1251
+ if (terminated) {
1252
+ this.warn("Unreachable code after return/break/continue", stmt.loc || node.loc, null, { code: 'W201' });
1253
+ break; // Only warn once per block
1254
+ }
999
1255
  this.visitNode(stmt);
1256
+ if (stmt.type === 'ReturnStatement' || stmt.type === 'BreakStatement' || stmt.type === 'ContinueStatement') {
1257
+ terminated = true;
1258
+ }
1000
1259
  }
1001
1260
  } finally {
1002
1261
  if (node.loc) {
@@ -1007,21 +1266,193 @@ export class Analyzer {
1007
1266
  }
1008
1267
 
1009
1268
  visitIfStatement(node) {
1269
+ // Constant conditional check
1270
+ if (node.condition && node.condition.type === 'BooleanLiteral') {
1271
+ if (node.condition.value === true) {
1272
+ this.warn("Condition is always true", node.condition.loc || node.loc, null, { code: 'W202' });
1273
+ } else {
1274
+ this.warn("Condition is always false — branch never executes", node.condition.loc || node.loc, null, { code: 'W203' });
1275
+ }
1276
+ }
1277
+
1010
1278
  this.visitExpression(node.condition);
1011
- this.visitNode(node.consequent);
1279
+
1280
+ // Type narrowing: detect patterns like typeOf(x) == "String", x != nil, x.isOk()
1281
+ const narrowing = this._extractNarrowingInfo(node.condition);
1282
+
1283
+ // Visit consequent with narrowed type
1284
+ if (narrowing) {
1285
+ const prevScope = this.currentScope;
1286
+ this.currentScope = this.currentScope.child('block');
1287
+ const sym = this.currentScope.lookup(narrowing.varName);
1288
+ if (sym) {
1289
+ // Store narrowed type info in the scope
1290
+ const narrowedSym = new Symbol(narrowing.varName, sym.kind, null, false, sym.loc);
1291
+ narrowedSym.inferredType = narrowing.narrowedType;
1292
+ narrowedSym._narrowed = true;
1293
+ try { this.currentScope.define(narrowing.varName, narrowedSym); } catch (e) { /* already defined */ }
1294
+ }
1295
+ for (const stmt of node.consequent.body) {
1296
+ this.visitNode(stmt);
1297
+ }
1298
+ this.currentScope = prevScope;
1299
+ } else {
1300
+ this.visitNode(node.consequent);
1301
+ }
1302
+
1012
1303
  for (const alt of node.alternates) {
1013
1304
  this.visitExpression(alt.condition);
1014
1305
  this.visitNode(alt.body);
1015
1306
  }
1307
+
1308
+ // Visit else body with inverse narrowing
1016
1309
  if (node.elseBody) {
1017
- this.visitNode(node.elseBody);
1310
+ if (narrowing && narrowing.inverseType) {
1311
+ const prevScope = this.currentScope;
1312
+ this.currentScope = this.currentScope.child('block');
1313
+ const sym = this.currentScope.lookup(narrowing.varName);
1314
+ if (sym) {
1315
+ const narrowedSym = new Symbol(narrowing.varName, sym.kind, null, false, sym.loc);
1316
+ narrowedSym.inferredType = narrowing.inverseType;
1317
+ narrowedSym._narrowed = true;
1318
+ try { this.currentScope.define(narrowing.varName, narrowedSym); } catch (e) { /* already defined */ }
1319
+ }
1320
+ for (const stmt of node.elseBody.body) {
1321
+ this.visitNode(stmt);
1322
+ }
1323
+ this.currentScope = prevScope;
1324
+ } else {
1325
+ this.visitNode(node.elseBody);
1326
+ }
1018
1327
  }
1019
1328
  }
1020
1329
 
1330
+ _extractNarrowingInfo(condition) {
1331
+ if (!condition) return null;
1332
+
1333
+ // Pattern: typeOf(x) == "String" or typeOf(x) == "Int"
1334
+ if (condition.type === 'BinaryExpression' && condition.operator === '==') {
1335
+ const { left, right } = condition;
1336
+
1337
+ // typeOf(x) == "TypeName"
1338
+ if (left.type === 'CallExpression' &&
1339
+ left.callee.type === 'Identifier' &&
1340
+ (left.callee.name === 'typeOf' || left.callee.name === 'type_of') &&
1341
+ left.arguments.length === 1 &&
1342
+ left.arguments[0].type === 'Identifier' &&
1343
+ right.type === 'StringLiteral') {
1344
+ const varName = left.arguments[0].name;
1345
+ const typeName = right.value;
1346
+ // Map JS typeof strings to Tova types
1347
+ const typeMap = { 'string': 'String', 'number': 'Int', 'boolean': 'Bool', 'function': 'Function' };
1348
+ const narrowedType = typeMap[typeName] || typeName;
1349
+ return { varName, narrowedType, inverseType: null };
1350
+ }
1351
+
1352
+ // "TypeName" == typeOf(x) (reversed)
1353
+ if (right.type === 'CallExpression' &&
1354
+ right.callee.type === 'Identifier' &&
1355
+ (right.callee.name === 'typeOf' || right.callee.name === 'type_of') &&
1356
+ right.arguments.length === 1 &&
1357
+ right.arguments[0].type === 'Identifier' &&
1358
+ left.type === 'StringLiteral') {
1359
+ const varName = right.arguments[0].name;
1360
+ const typeName = left.value;
1361
+ const typeMap = { 'string': 'String', 'number': 'Int', 'boolean': 'Bool', 'function': 'Function' };
1362
+ const narrowedType = typeMap[typeName] || typeName;
1363
+ return { varName, narrowedType, inverseType: null };
1364
+ }
1365
+ }
1366
+
1367
+ // Pattern: x != nil (narrow to non-nil)
1368
+ if (condition.type === 'BinaryExpression' && condition.operator === '!=' &&
1369
+ condition.right.type === 'NilLiteral' &&
1370
+ condition.left.type === 'Identifier') {
1371
+ // Try to compute a precise narrowed type by stripping Nil from the variable's type
1372
+ const varName = condition.left.name;
1373
+ const sym = this.currentScope.lookup(varName);
1374
+ let narrowedType = 'nonnil';
1375
+ if (sym && sym.inferredType) {
1376
+ const stripped = this._stripNilFromType(sym.inferredType);
1377
+ if (stripped) narrowedType = stripped;
1378
+ }
1379
+ return { varName, narrowedType, inverseType: 'Nil' };
1380
+ }
1381
+
1382
+ // Pattern: nil != x (reversed)
1383
+ if (condition.type === 'BinaryExpression' && condition.operator === '!=' &&
1384
+ condition.left.type === 'NilLiteral' &&
1385
+ condition.right.type === 'Identifier') {
1386
+ const varName = condition.right.name;
1387
+ const sym = this.currentScope.lookup(varName);
1388
+ let narrowedType = 'nonnil';
1389
+ if (sym && sym.inferredType) {
1390
+ const stripped = this._stripNilFromType(sym.inferredType);
1391
+ if (stripped) narrowedType = stripped;
1392
+ }
1393
+ return { varName, narrowedType, inverseType: 'Nil' };
1394
+ }
1395
+
1396
+ // Pattern: x == nil (narrow to Nil in consequent, non-nil in else)
1397
+ if (condition.type === 'BinaryExpression' && condition.operator === '==' &&
1398
+ condition.right.type === 'NilLiteral' &&
1399
+ condition.left.type === 'Identifier') {
1400
+ const varName = condition.left.name;
1401
+ const sym = this.currentScope.lookup(varName);
1402
+ let inverseType = 'nonnil';
1403
+ if (sym && sym.inferredType) {
1404
+ const stripped = this._stripNilFromType(sym.inferredType);
1405
+ if (stripped) inverseType = stripped;
1406
+ }
1407
+ return { varName, narrowedType: 'Nil', inverseType };
1408
+ }
1409
+
1410
+ // Pattern: x.isOk() (narrow to Ok variant)
1411
+ if (condition.type === 'CallExpression' &&
1412
+ condition.callee.type === 'MemberExpression' &&
1413
+ condition.callee.object.type === 'Identifier' &&
1414
+ condition.callee.property === 'isOk') {
1415
+ return { varName: condition.callee.object.name, narrowedType: 'Result<Ok>', inverseType: 'Result<Err>' };
1416
+ }
1417
+
1418
+ // Pattern: x.isSome() (narrow to Some variant)
1419
+ if (condition.type === 'CallExpression' &&
1420
+ condition.callee.type === 'MemberExpression' &&
1421
+ condition.callee.object.type === 'Identifier' &&
1422
+ condition.callee.property === 'isSome') {
1423
+ return { varName: condition.callee.object.name, narrowedType: 'Option<Some>', inverseType: 'Option<None>' };
1424
+ }
1425
+
1426
+ return null;
1427
+ }
1428
+
1429
+ /**
1430
+ * Strip Nil from a union type string. E.g., "String | Nil" -> "String".
1431
+ * For Option types, returns the inner type.
1432
+ */
1433
+ _stripNilFromType(typeStr) {
1434
+ if (!typeStr) return null;
1435
+ // Handle union types: "String | Nil" -> "String"
1436
+ if (typeStr.includes(' | ')) {
1437
+ const parts = typeStr.split(' | ').map(p => p.trim()).filter(p => p !== 'Nil');
1438
+ if (parts.length === 0) return null;
1439
+ if (parts.length === 1) return parts[0];
1440
+ return parts.join(' | ');
1441
+ }
1442
+ // Handle Option types: "Option<String>" -> "String"
1443
+ if (typeStr.startsWith('Option<') && typeStr.endsWith('>')) {
1444
+ return typeStr.slice(7, -1);
1445
+ }
1446
+ if (typeStr === 'Option') return 'Any';
1447
+ // If not a union/option, narrowing past nil means the variable keeps its type
1448
+ return typeStr;
1449
+ }
1450
+
1021
1451
  visitForStatement(node) {
1022
1452
  const prevScope = this.currentScope;
1023
1453
  this.currentScope = this.currentScope.child('block');
1024
1454
  this.currentScope._isLoop = true;
1455
+ if (node.label) this.currentScope._loopLabel = node.label;
1025
1456
 
1026
1457
  try {
1027
1458
  this.visitExpression(node.iterable);
@@ -1037,6 +1468,10 @@ export class Analyzer {
1037
1468
  }
1038
1469
  }
1039
1470
 
1471
+ if (node.guard) {
1472
+ this.visitExpression(node.guard);
1473
+ }
1474
+
1040
1475
  this.visitNode(node.body);
1041
1476
  } finally {
1042
1477
  this.currentScope = prevScope;
@@ -1048,10 +1483,28 @@ export class Analyzer {
1048
1483
  }
1049
1484
 
1050
1485
  visitWhileStatement(node) {
1486
+ // while false is suspicious (loop body never executes)
1487
+ if (node.condition && node.condition.type === 'BooleanLiteral' && node.condition.value === false) {
1488
+ this.warn("Condition is always false — loop never executes", node.condition.loc || node.loc, null, { code: 'W203' });
1489
+ }
1490
+
1051
1491
  this.visitExpression(node.condition);
1052
1492
  const prevScope = this.currentScope;
1053
1493
  this.currentScope = this.currentScope.child('block');
1054
1494
  this.currentScope._isLoop = true;
1495
+ if (node.label) this.currentScope._loopLabel = node.label;
1496
+ try {
1497
+ this.visitNode(node.body);
1498
+ } finally {
1499
+ this.currentScope = prevScope;
1500
+ }
1501
+ }
1502
+
1503
+ visitLoopStatement(node) {
1504
+ const prevScope = this.currentScope;
1505
+ this.currentScope = this.currentScope.child('block');
1506
+ this.currentScope._isLoop = true;
1507
+ if (node.label) this.currentScope._loopLabel = node.label;
1055
1508
  try {
1056
1509
  this.visitNode(node.body);
1057
1510
  } finally {
@@ -1097,7 +1550,7 @@ export class Analyzer {
1097
1550
  }
1098
1551
  // Return must be inside a function
1099
1552
  if (this._functionReturnTypeStack.length === 0) {
1100
- this.error("'return' can only be used inside a function", node.loc);
1553
+ this.error("'return' can only be used inside a function", node.loc, null, { code: 'E301' });
1101
1554
  return;
1102
1555
  }
1103
1556
  // Check return type against declared function return type
@@ -1106,7 +1559,7 @@ export class Analyzer {
1106
1559
  if (expectedReturn) {
1107
1560
  const actualType = node.value ? this._inferType(node.value) : 'Nil';
1108
1561
  if (!this._typesCompatible(expectedReturn, actualType)) {
1109
- this.error(`Type mismatch: function expects return type ${expectedReturn}, but got ${actualType}`, node.loc);
1562
+ this.error(`Type mismatch: function expects return type ${expectedReturn}, but got ${actualType}`, node.loc, this._conversionHint(expectedReturn, actualType), { code: 'E101' });
1110
1563
  }
1111
1564
  }
1112
1565
  }
@@ -1117,7 +1570,7 @@ export class Analyzer {
1117
1570
  if (node.target.type === 'Identifier') {
1118
1571
  const sym = this.currentScope.lookup(node.target.name);
1119
1572
  if (sym && !sym.mutable && sym.kind !== 'builtin') {
1120
- this.error(`Cannot use '${node.operator}' on immutable variable '${node.target.name}'`, node.loc);
1573
+ this.error(`Cannot use '${node.operator}' on immutable variable '${node.target.name}'`, node.loc, `declare with 'var' to make mutable`, { code: 'E202' });
1121
1574
  }
1122
1575
  // Type check compound assignment
1123
1576
  if (sym && sym.inferredType) {
@@ -1151,471 +1604,40 @@ export class Analyzer {
1151
1604
  this.visitExpression(node.value);
1152
1605
  }
1153
1606
 
1154
- // ─── Client-specific visitors ─────────────────────────────
1155
-
1156
- visitStateDeclaration(node) {
1157
- const ctx = this.currentScope.getContext();
1158
- if (ctx !== 'client') {
1159
- this.error(`'state' can only be used inside a client block`, node.loc);
1160
- }
1161
- try {
1162
- this.currentScope.define(node.name,
1163
- new Symbol(node.name, 'state', node.typeAnnotation, true, node.loc));
1164
- } catch (e) {
1165
- this.error(e.message);
1166
- }
1167
- this.visitExpression(node.initialValue);
1168
- }
1169
-
1170
- visitComputedDeclaration(node) {
1171
- const ctx = this.currentScope.getContext();
1172
- if (ctx !== 'client') {
1173
- this.error(`'computed' can only be used inside a client block`, node.loc);
1174
- }
1175
- try {
1176
- this.currentScope.define(node.name,
1177
- new Symbol(node.name, 'computed', null, false, node.loc));
1178
- } catch (e) {
1179
- this.error(e.message);
1180
- }
1181
- this.visitExpression(node.expression);
1182
- }
1183
-
1184
- visitEffectDeclaration(node) {
1185
- const ctx = this.currentScope.getContext();
1186
- if (ctx !== 'client') {
1187
- this.error(`'effect' can only be used inside a client block`, node.loc);
1188
- }
1189
- this.visitNode(node.body);
1190
- }
1191
-
1192
- visitComponentDeclaration(node) {
1193
- const ctx = this.currentScope.getContext();
1194
- if (ctx !== 'client') {
1195
- this.error(`'component' can only be used inside a client block`, node.loc);
1196
- }
1197
- try {
1198
- this.currentScope.define(node.name,
1199
- new Symbol(node.name, 'component', null, false, node.loc));
1200
- } catch (e) {
1201
- this.error(e.message);
1202
- }
1203
-
1204
- const prevScope = this.currentScope;
1205
- this.currentScope = this.currentScope.child('function');
1206
- for (const param of node.params) {
1207
- try {
1208
- this.currentScope.define(param.name,
1209
- new Symbol(param.name, 'parameter', param.typeAnnotation, false, param.loc));
1210
- } catch (e) {
1211
- this.error(e.message);
1212
- }
1213
- }
1214
- for (const child of node.body) {
1215
- this.visitNode(child);
1216
- }
1217
- this.currentScope = prevScope;
1218
- }
1219
-
1220
- visitStoreDeclaration(node) {
1221
- const ctx = this.currentScope.getContext();
1222
- if (ctx !== 'client') {
1223
- this.error(`'store' can only be used inside a client block`, node.loc);
1224
- }
1225
- try {
1226
- this.currentScope.define(node.name,
1227
- new Symbol(node.name, 'variable', null, false, node.loc));
1228
- } catch (e) {
1229
- this.error(e.message);
1230
- }
1607
+ // Client-specific visitors (visitState, visitComputed, etc.) are in client-analyzer.js (lazy-loaded)
1231
1608
 
1609
+ visitTestBlock(node) {
1232
1610
  const prevScope = this.currentScope;
1233
1611
  this.currentScope = this.currentScope.child('block');
1234
- for (const child of node.body) {
1235
- this.visitNode(child);
1236
- }
1237
- this.currentScope = prevScope;
1238
- }
1239
-
1240
- visitRouteDeclaration(node) {
1241
- const ctx = this.currentScope.getContext();
1242
- if (ctx !== 'server') {
1243
- this.error(`'route' can only be used inside a server block`, node.loc);
1244
- }
1245
- this.visitExpression(node.handler);
1246
-
1247
- // Route param ↔ handler signature type safety
1248
- if (node.handler.type === 'Identifier') {
1249
- const handlerName = node.handler.name;
1250
- // Find the function declaration in the current server block scope
1251
- const fnSym = this.currentScope.lookup(handlerName);
1252
- if (fnSym && fnSym.kind === 'function' && fnSym._params) {
1253
- const pathParams = new Set();
1254
- const pathStr = node.path || '';
1255
- const paramMatches = pathStr.match(/:([a-zA-Z_][a-zA-Z0-9_]*)/g);
1256
- if (paramMatches) {
1257
- for (const m of paramMatches) pathParams.add(m.slice(1));
1258
- }
1259
- const handlerParams = fnSym._params.filter(p => p !== 'req');
1260
- for (const hp of handlerParams) {
1261
- if (pathParams.size > 0 && !pathParams.has(hp) && node.method.toUpperCase() === 'GET') {
1262
- // For GET routes, params not in path come from query — this is fine, just a warning
1263
- this.warn(`Handler '${handlerName}' param '${hp}' not in route path '${pathStr}' — will be extracted from query string`, node.loc);
1264
- }
1265
- }
1266
- }
1267
- }
1268
- }
1269
-
1270
- visitMiddlewareDeclaration(node) {
1271
- const ctx = this.currentScope.getContext();
1272
- if (ctx !== 'server') {
1273
- this.error(`'middleware' can only be used inside a server block`, node.loc);
1274
- }
1275
1612
  try {
1276
- this.currentScope.define(node.name,
1277
- new Symbol(node.name, 'function', null, false, node.loc));
1278
- } catch (e) {
1279
- this.error(e.message);
1280
- }
1281
- const prevScope = this.currentScope;
1282
- this.currentScope = this.currentScope.child('function');
1283
- for (const param of node.params) {
1284
- try {
1285
- this.currentScope.define(param.name,
1286
- new Symbol(param.name, 'parameter', param.typeAnnotation, false, param.loc));
1287
- } catch (e) {
1288
- this.error(e.message);
1289
- }
1290
- }
1291
- this.visitNode(node.body);
1292
- this.currentScope = prevScope;
1293
- }
1294
-
1295
- visitHealthCheckDeclaration(node) {
1296
- const ctx = this.currentScope.getContext();
1297
- if (ctx !== 'server') {
1298
- this.error(`'health' can only be used inside a server block`, node.loc);
1299
- }
1300
- }
1301
-
1302
- visitCorsDeclaration(node) {
1303
- const ctx = this.currentScope.getContext();
1304
- if (ctx !== 'server') {
1305
- this.error(`'cors' can only be used inside a server block`, node.loc);
1306
- }
1307
- for (const value of Object.values(node.config)) {
1308
- this.visitExpression(value);
1309
- }
1310
- }
1311
-
1312
- visitErrorHandlerDeclaration(node) {
1313
- const ctx = this.currentScope.getContext();
1314
- if (ctx !== 'server') {
1315
- this.error(`'on_error' can only be used inside a server block`, node.loc);
1316
- }
1317
- const prevScope = this.currentScope;
1318
- this.currentScope = this.currentScope.child('function');
1319
- for (const param of node.params) {
1320
- try {
1321
- this.currentScope.define(param.name,
1322
- new Symbol(param.name, 'parameter', param.typeAnnotation, false, param.loc));
1323
- } catch (e) {
1324
- this.error(e.message);
1325
- }
1326
- }
1327
- this.visitNode(node.body);
1328
- this.currentScope = prevScope;
1329
- }
1330
-
1331
- visitWebSocketDeclaration(node) {
1332
- const ctx = this.currentScope.getContext();
1333
- if (ctx !== 'server') {
1334
- this.error(`'ws' can only be used inside a server block`, node.loc);
1335
- }
1336
- for (const [, handler] of Object.entries(node.handlers)) {
1337
- if (!handler) continue;
1338
- const prevScope = this.currentScope;
1339
- this.currentScope = this.currentScope.child('function');
1340
- for (const param of handler.params) {
1341
- try {
1342
- this.currentScope.define(param.name,
1343
- new Symbol(param.name, 'parameter', param.typeAnnotation, false, param.loc));
1344
- } catch (e) {
1345
- this.error(e.message);
1346
- }
1613
+ for (const stmt of node.body) {
1614
+ this.visitNode(stmt);
1347
1615
  }
1348
- this.visitNode(handler.body);
1616
+ } finally {
1349
1617
  this.currentScope = prevScope;
1350
1618
  }
1351
1619
  }
1352
1620
 
1353
- visitStaticDeclaration(node) {
1354
- const ctx = this.currentScope.getContext();
1355
- if (ctx !== 'server') {
1356
- this.error(`'static' can only be used inside a server block`, node.loc);
1357
- }
1358
- }
1359
-
1360
- visitDiscoverDeclaration(node) {
1361
- const ctx = this.currentScope.getContext();
1362
- if (ctx !== 'server') {
1363
- this.error(`'discover' can only be used inside a server block`, node.loc);
1364
- }
1365
- this.visitExpression(node.urlExpression);
1366
- }
1367
-
1368
- visitAuthDeclaration(node) {
1369
- const ctx = this.currentScope.getContext();
1370
- if (ctx !== 'server') {
1371
- this.error(`'auth' can only be used inside a server block`, node.loc);
1372
- }
1373
- for (const value of Object.values(node.config)) {
1374
- this.visitExpression(value);
1375
- }
1376
- }
1377
-
1378
- visitMaxBodyDeclaration(node) {
1379
- const ctx = this.currentScope.getContext();
1380
- if (ctx !== 'server') {
1381
- this.error(`'max_body' can only be used inside a server block`, node.loc);
1382
- }
1383
- this.visitExpression(node.limit);
1384
- }
1385
-
1386
- visitRouteGroupDeclaration(node) {
1387
- const ctx = this.currentScope.getContext();
1388
- if (ctx !== 'server') {
1389
- this.error(`'routes' can only be used inside a server block`, node.loc);
1390
- }
1391
- for (const stmt of node.body) {
1392
- this.visitNode(stmt);
1393
- }
1394
- }
1395
-
1396
- visitRateLimitDeclaration(node) {
1397
- const ctx = this.currentScope.getContext();
1398
- if (ctx !== 'server') {
1399
- this.error(`'rate_limit' can only be used inside a server block`, node.loc);
1400
- }
1401
- for (const value of Object.values(node.config)) {
1402
- this.visitExpression(value);
1403
- }
1404
- }
1405
-
1406
- visitLifecycleHookDeclaration(node) {
1407
- const ctx = this.currentScope.getContext();
1408
- if (ctx !== 'server') {
1409
- this.error(`'on_${node.hook}' can only be used inside a server block`, node.loc);
1410
- }
1411
- const prevScope = this.currentScope;
1412
- this.currentScope = this.currentScope.child('function');
1413
- for (const param of node.params) {
1414
- try {
1415
- this.currentScope.define(param.name,
1416
- new Symbol(param.name, 'parameter', param.typeAnnotation, false, param.loc));
1417
- } catch (e) {
1418
- this.error(e.message);
1419
- }
1420
- }
1421
- this.visitNode(node.body);
1422
- this.currentScope = prevScope;
1423
- }
1424
-
1425
- visitSubscribeDeclaration(node) {
1426
- const ctx = this.currentScope.getContext();
1427
- if (ctx !== 'server') {
1428
- this.error(`'subscribe' can only be used inside a server block`, node.loc);
1429
- }
1430
- const prevScope = this.currentScope;
1431
- this.currentScope = this.currentScope.child('function');
1432
- for (const param of node.params) {
1433
- try {
1434
- this.currentScope.define(param.name,
1435
- new Symbol(param.name, 'parameter', param.typeAnnotation, false, param.loc));
1436
- } catch (e) {
1437
- this.error(e.message);
1438
- }
1439
- }
1440
- this.visitNode(node.body);
1441
- this.currentScope = prevScope;
1442
- }
1443
-
1444
- visitEnvDeclaration(node) {
1445
- const ctx = this.currentScope.getContext();
1446
- if (ctx !== 'server') {
1447
- this.error(`'env' can only be used inside a server block`, node.loc);
1448
- }
1449
- try {
1450
- this.currentScope.define(node.name,
1451
- new Symbol(node.name, 'variable', node.typeAnnotation, false, node.loc));
1452
- } catch (e) {
1453
- this.error(e.message);
1454
- }
1455
- if (node.defaultValue) {
1456
- this.visitExpression(node.defaultValue);
1457
- }
1458
- }
1459
-
1460
- visitScheduleDeclaration(node) {
1461
- const ctx = this.currentScope.getContext();
1462
- if (ctx !== 'server') {
1463
- this.error(`'schedule' can only be used inside a server block`, node.loc);
1464
- }
1465
- if (node.name) {
1466
- try {
1467
- this.currentScope.define(node.name,
1468
- new Symbol(node.name, 'function', null, false, node.loc));
1469
- } catch (e) {
1470
- this.error(e.message);
1471
- }
1472
- }
1473
- const prevScope = this.currentScope;
1474
- this.currentScope = this.currentScope.child('function');
1475
- for (const param of node.params) {
1476
- try {
1477
- this.currentScope.define(param.name,
1478
- new Symbol(param.name, 'parameter', param.typeAnnotation, false, param.loc));
1479
- } catch (e) {
1480
- this.error(e.message);
1481
- }
1482
- }
1483
- this.visitNode(node.body);
1484
- this.currentScope = prevScope;
1485
- }
1486
-
1487
- visitUploadDeclaration(node) {
1488
- const ctx = this.currentScope.getContext();
1489
- if (ctx !== 'server') {
1490
- this.error(`'upload' can only be used inside a server block`, node.loc);
1491
- }
1492
- for (const value of Object.values(node.config)) {
1493
- this.visitExpression(value);
1494
- }
1495
- }
1496
-
1497
- visitSessionDeclaration(node) {
1498
- const ctx = this.currentScope.getContext();
1499
- if (ctx !== 'server') {
1500
- this.error(`'session' can only be used inside a server block`, node.loc);
1501
- }
1502
- for (const value of Object.values(node.config)) {
1503
- this.visitExpression(value);
1504
- }
1505
- }
1506
-
1507
- visitDbDeclaration(node) {
1508
- const ctx = this.currentScope.getContext();
1509
- if (ctx !== 'server') {
1510
- this.error(`'db' can only be used inside a server block`, node.loc);
1511
- }
1512
- for (const value of Object.values(node.config)) {
1513
- this.visitExpression(value);
1514
- }
1515
- }
1516
-
1517
- visitTlsDeclaration(node) {
1518
- const ctx = this.currentScope.getContext();
1519
- if (ctx !== 'server') {
1520
- this.error(`'tls' can only be used inside a server block`, node.loc);
1521
- }
1522
- for (const value of Object.values(node.config)) {
1523
- this.visitExpression(value);
1524
- }
1525
- }
1526
-
1527
- visitCompressionDeclaration(node) {
1528
- const ctx = this.currentScope.getContext();
1529
- if (ctx !== 'server') {
1530
- this.error(`'compression' can only be used inside a server block`, node.loc);
1531
- }
1532
- for (const value of Object.values(node.config)) {
1533
- this.visitExpression(value);
1534
- }
1535
- }
1536
-
1537
- visitBackgroundJobDeclaration(node) {
1538
- const ctx = this.currentScope.getContext();
1539
- if (ctx !== 'server') {
1540
- this.error(`'background' can only be used inside a server block`, node.loc);
1541
- }
1542
- try {
1543
- this.currentScope.define(node.name,
1544
- new Symbol(node.name, 'function', null, false, node.loc));
1545
- } catch (e) {
1546
- this.error(e.message);
1547
- }
1548
- const prevScope = this.currentScope;
1549
- this.currentScope = this.currentScope.child('function');
1550
- for (const param of node.params) {
1551
- try {
1552
- this.currentScope.define(param.name,
1553
- new Symbol(param.name, 'parameter', param.typeAnnotation, false, param.loc));
1554
- } catch (e) {
1555
- this.error(e.message);
1556
- }
1557
- }
1558
- this.visitNode(node.body);
1559
- this.currentScope = prevScope;
1560
- }
1561
-
1562
- visitCacheDeclaration(node) {
1563
- const ctx = this.currentScope.getContext();
1564
- if (ctx !== 'server') {
1565
- this.error(`'cache' can only be used inside a server block`, node.loc);
1566
- }
1567
- for (const value of Object.values(node.config)) {
1568
- this.visitExpression(value);
1569
- }
1570
- }
1571
-
1572
- visitSseDeclaration(node) {
1573
- const ctx = this.currentScope.getContext();
1574
- if (ctx !== 'server') {
1575
- this.error(`'sse' can only be used inside a server block`, node.loc);
1576
- }
1577
- const prevScope = this.currentScope;
1578
- this.currentScope = this.currentScope.child('block');
1579
- for (const p of node.params) {
1580
- this.currentScope.define(p.name, { kind: 'param' });
1581
- }
1582
- for (const stmt of node.body.body || []) {
1583
- this.visitNode(stmt);
1584
- }
1585
- this.currentScope = prevScope;
1586
- }
1587
-
1588
- visitModelDeclaration(node) {
1589
- const ctx = this.currentScope.getContext();
1590
- if (ctx !== 'server') {
1591
- this.error(`'model' can only be used inside a server block`, node.loc);
1592
- }
1593
- if (node.config) {
1594
- for (const value of Object.values(node.config)) {
1595
- this.visitExpression(value);
1596
- }
1597
- }
1598
- }
1599
-
1600
- visitTestBlock(node) {
1601
- const prevScope = this.currentScope;
1602
- this.currentScope = this.currentScope.child('block');
1603
- for (const stmt of node.body) {
1604
- this.visitNode(stmt);
1605
- }
1606
- this.currentScope = prevScope;
1607
- }
1608
-
1609
1621
  // ─── Expression visitors ──────────────────────────────────
1610
1622
 
1611
1623
  visitIdentifier(node) {
1612
1624
  if (node.name === '_') return; // wildcard is always valid
1613
1625
  if (node.name === PIPE_TARGET) return; // pipe target placeholder from method pipe
1614
1626
 
1627
+ // Common mistake: using `throw` (not a Tova keyword)
1628
+ if (node.name === 'throw') {
1629
+ this.warn("'throw' is not a Tova keyword — use Result for error handling, e.g. Err(\"message\")", node.loc, "try Err(value) instead of throw", { code: 'W206' });
1630
+ return;
1631
+ }
1632
+
1615
1633
  const sym = this.currentScope.lookup(node.name);
1616
1634
  if (!sym) {
1617
1635
  if (!this._isKnownGlobal(node.name)) {
1618
- this.warn(`'${node.name}' is not defined`, node.loc);
1636
+ const suggestion = this._findClosestMatch(node.name);
1637
+ const hint = suggestion ? `did you mean '${suggestion}'?` : null;
1638
+ const fixOpts = { code: 'E200', length: node.name.length };
1639
+ if (suggestion) fixOpts.fix = { description: `Replace with '${suggestion}'`, replacement: suggestion };
1640
+ this.warn(`'${node.name}' is not defined`, node.loc, hint, fixOpts);
1619
1641
  }
1620
1642
  } else {
1621
1643
  sym.used = true;
@@ -1633,13 +1655,60 @@ export class Analyzer {
1633
1655
  return _JS_GLOBALS.has(name);
1634
1656
  }
1635
1657
 
1658
+ _findClosestMatch(name) {
1659
+ const candidates = new Set();
1660
+ let scope = this.currentScope;
1661
+ while (scope) {
1662
+ for (const n of scope.symbols.keys()) candidates.add(n);
1663
+ scope = scope.parent;
1664
+ }
1665
+ for (const n of BUILTIN_NAMES) candidates.add(n);
1666
+ for (const n of _JS_GLOBALS) candidates.add(n);
1667
+ for (const n of _TOVA_RUNTIME) candidates.add(n);
1668
+
1669
+ let best = null;
1670
+ let bestDist = Infinity;
1671
+ const maxDist = Math.max(2, Math.floor(name.length * 0.4));
1672
+ for (const c of candidates) {
1673
+ if (Math.abs(c.length - name.length) > maxDist) continue;
1674
+ const d = levenshtein(name.toLowerCase(), c.toLowerCase());
1675
+ if (d < bestDist && d <= maxDist && d > 0) {
1676
+ bestDist = d;
1677
+ best = c;
1678
+ }
1679
+ }
1680
+ return best;
1681
+ }
1682
+
1683
+ _conversionHint(expected, actual) {
1684
+ if (!expected || !actual) return null;
1685
+ const key = `${actual}->${expected}`;
1686
+ const hints = {
1687
+ 'Int->String': "try toString(value) to convert",
1688
+ 'Float->String': "try toString(value) to convert",
1689
+ 'Bool->String': "try toString(value) to convert",
1690
+ 'String->Int': "try toInt(value) to parse",
1691
+ 'String->Float': "try toFloat(value) to parse",
1692
+ 'Float->Int': "try floor(value) or round(value) to convert",
1693
+ };
1694
+ if (hints[key]) return hints[key];
1695
+ if (expected.startsWith('Result')) return "try Ok(value) to wrap in Result";
1696
+ if (expected.startsWith('Option')) return "try Some(value) to wrap in Option";
1697
+ return null;
1698
+ }
1699
+
1636
1700
  visitLambda(node) {
1637
1701
  const prevScope = this.currentScope;
1638
1702
  this.currentScope = this.currentScope.child('function');
1639
1703
 
1640
1704
  const expectedReturn = node.returnType ? this._typeAnnotationToString(node.returnType) : null;
1641
1705
  this._functionReturnTypeStack.push(expectedReturn);
1642
- if (node.isAsync) this._asyncDepth++;
1706
+ const prevAsyncDepth = this._asyncDepth;
1707
+ if (node.isAsync) {
1708
+ this._asyncDepth++;
1709
+ } else {
1710
+ this._asyncDepth = 0; // Non-async lambda resets async context
1711
+ }
1643
1712
 
1644
1713
  try {
1645
1714
  for (const param of node.params) {
@@ -1655,14 +1724,14 @@ export class Analyzer {
1655
1724
  this.visitNode(node.body);
1656
1725
  // Return path analysis for lambdas with block bodies and declared return types
1657
1726
  if (expectedReturn && !this._definitelyReturns(node.body)) {
1658
- this.warn(`Lambda declares return type ${expectedReturn} but not all code paths return a value`, node.loc);
1727
+ this.warn(`Lambda declares return type ${expectedReturn} but not all code paths return a value`, node.loc, null, { code: 'W205' });
1659
1728
  }
1660
1729
  } else {
1661
1730
  // Single-expression body — always returns implicitly
1662
1731
  this.visitExpression(node.body);
1663
1732
  }
1664
1733
  } finally {
1665
- if (node.isAsync) this._asyncDepth--;
1734
+ this._asyncDepth = prevAsyncDepth;
1666
1735
  this._functionReturnTypeStack.pop();
1667
1736
  this.currentScope = prevScope;
1668
1737
  }
@@ -1670,7 +1739,14 @@ export class Analyzer {
1670
1739
 
1671
1740
  visitMatchExpression(node) {
1672
1741
  this.visitExpression(node.subject);
1742
+ let catchAllSeen = false;
1673
1743
  for (const arm of node.arms) {
1744
+ // Warn about unreachable arms after catch-all
1745
+ if (catchAllSeen) {
1746
+ this.warn("Unreachable match arm after catch-all pattern", arm.pattern.loc || arm.body.loc || node.loc, null, { code: 'W207' });
1747
+ continue; // Still visit remaining arms for completeness but skip analysis
1748
+ }
1749
+
1674
1750
  const prevScope = this.currentScope;
1675
1751
  this.currentScope = this.currentScope.child('block');
1676
1752
 
@@ -1686,6 +1762,11 @@ export class Analyzer {
1686
1762
  } finally {
1687
1763
  this.currentScope = prevScope;
1688
1764
  }
1765
+
1766
+ // Check if this arm is a catch-all (wildcard or unguarded binding)
1767
+ if ((arm.pattern.type === 'WildcardPattern' || arm.pattern.type === 'BindingPattern') && !arm.guard) {
1768
+ catchAllSeen = true;
1769
+ }
1689
1770
  }
1690
1771
 
1691
1772
  // Exhaustive match checking (#12)
@@ -1740,7 +1821,7 @@ export class Analyzer {
1740
1821
  const allVariants = subjectType.getVariantNames();
1741
1822
  for (const v of allVariants) {
1742
1823
  if (!coveredVariants.has(v)) {
1743
- this.warn(`Non-exhaustive match: missing '${v}' variant from type '${subjectType.name}'`, node.loc);
1824
+ this.warn(`Non-exhaustive match: missing '${v}' variant from type '${subjectType.name}'`, node.loc, `add a '${v}' arm or use '_ =>' as a catch-all`, { code: 'W200' });
1744
1825
  }
1745
1826
  }
1746
1827
  return; // Done — used precise ADT checking
@@ -1749,46 +1830,50 @@ export class Analyzer {
1749
1830
  // Check built-in Result/Option types
1750
1831
  if (coveredVariants.has('Ok') || coveredVariants.has('Err')) {
1751
1832
  if (!coveredVariants.has('Ok')) {
1752
- this.warn(`Non-exhaustive match: missing 'Ok' variant`, node.loc);
1833
+ this.warn(`Non-exhaustive match: missing 'Ok' variant`, node.loc, "add an 'Ok(value) =>' arm", { code: 'W200' });
1753
1834
  }
1754
1835
  if (!coveredVariants.has('Err')) {
1755
- this.warn(`Non-exhaustive match: missing 'Err' variant`, node.loc);
1836
+ this.warn(`Non-exhaustive match: missing 'Err' variant`, node.loc, "add an 'Err(e) =>' arm", { code: 'W200' });
1756
1837
  }
1757
1838
  }
1758
1839
  if (coveredVariants.has('Some') || coveredVariants.has('None')) {
1759
1840
  if (!coveredVariants.has('Some')) {
1760
- this.warn(`Non-exhaustive match: missing 'Some' variant`, node.loc);
1841
+ this.warn(`Non-exhaustive match: missing 'Some' variant`, node.loc, "add a 'Some(value) =>' arm", { code: 'W200' });
1761
1842
  }
1762
1843
  if (!coveredVariants.has('None')) {
1763
- this.warn(`Non-exhaustive match: missing 'None' variant`, node.loc);
1844
+ this.warn(`Non-exhaustive match: missing 'None' variant`, node.loc, "add a 'None =>' arm", { code: 'W200' });
1764
1845
  }
1765
1846
  }
1766
1847
 
1767
- // Check user-defined types — look up in _variantFields from the global scope
1768
- // Collect all known type variants by iterating type declarations
1769
- for (const topNode of this.ast.body) {
1770
- this._collectTypeVariants(topNode, variantNames, coveredVariants, node.loc);
1771
- }
1772
- }
1773
- }
1774
-
1775
- _collectTypeVariants(node, allVariants, coveredVariants, matchLoc) {
1776
- if (node.type === 'TypeDeclaration') {
1777
- const typeVariants = node.variants.filter(v => v.type === 'TypeVariant').map(v => v.name);
1778
- // If any of the match arms reference a variant from this type, check all
1779
- const relevantVariants = typeVariants.filter(v => coveredVariants.has(v));
1780
- if (relevantVariants.length > 0) {
1848
+ // Check user-defined types — find the single best-matching type whose variants
1849
+ // contain ALL covered variant names (avoids false positives with shared names)
1850
+ const candidates = [];
1851
+ this._collectTypeCandidates(this.ast.body, coveredVariants, candidates);
1852
+ // Only warn if exactly one type contains all covered variants
1853
+ if (candidates.length === 1) {
1854
+ const [typeName, typeVariants] = candidates[0];
1781
1855
  for (const v of typeVariants) {
1782
1856
  if (!coveredVariants.has(v)) {
1783
- this.warn(`Non-exhaustive match: missing '${v}' variant from type '${node.name}'`, matchLoc);
1857
+ this.warn(`Non-exhaustive match: missing '${v}' variant from type '${typeName}'`, node.loc, `add a '${v}' arm or use '_ =>' as a catch-all`, { code: 'W200' });
1784
1858
  }
1785
1859
  }
1786
1860
  }
1787
1861
  }
1788
- // Recurse into blocks
1789
- if (node.type === 'SharedBlock' || node.type === 'ServerBlock' || node.type === 'ClientBlock') {
1790
- for (const child of node.body) {
1791
- this._collectTypeVariants(child, allVariants, coveredVariants, matchLoc);
1862
+ }
1863
+
1864
+ _collectTypeCandidates(nodes, coveredVariants, candidates) {
1865
+ for (const node of nodes) {
1866
+ if (node.type === 'TypeDeclaration') {
1867
+ const typeVariants = node.variants.filter(v => v.type === 'TypeVariant').map(v => v.name);
1868
+ if (typeVariants.length === 0) continue;
1869
+ // All covered variants must be contained in this type's variants
1870
+ const allCovered = [...coveredVariants].every(v => typeVariants.includes(v));
1871
+ if (allCovered) {
1872
+ candidates.push([node.name, typeVariants]);
1873
+ }
1874
+ }
1875
+ if (node.type === 'SharedBlock' || node.type === 'ServerBlock' || node.type === 'ClientBlock') {
1876
+ this._collectTypeCandidates(node.body, coveredVariants, candidates);
1792
1877
  }
1793
1878
  }
1794
1879
  }
@@ -1882,77 +1967,23 @@ export class Analyzer {
1882
1967
  }
1883
1968
  }
1884
1969
 
1885
- visitJSXElement(node) {
1886
- for (const attr of node.attributes) {
1887
- if (attr.type === 'JSXSpreadAttribute') {
1888
- this.visitExpression(attr.expression);
1889
- } else {
1890
- this.visitExpression(attr.value);
1891
- }
1892
- }
1893
- for (const child of node.children) {
1894
- if (child.type === 'JSXElement') {
1895
- this.visitJSXElement(child);
1896
- } else if (child.type === 'JSXExpression') {
1897
- this.visitExpression(child.expression);
1898
- } else if (child.type === 'JSXFor') {
1899
- this.visitJSXFor(child);
1900
- } else if (child.type === 'JSXIf') {
1901
- this.visitJSXIf(child);
1902
- }
1903
- }
1904
- }
1905
-
1906
- visitJSXFor(node) {
1907
- const prevScope = this.currentScope;
1908
- this.currentScope = this.currentScope.child('block');
1909
- try {
1910
- this.visitExpression(node.iterable);
1911
- try {
1912
- this.currentScope.define(node.variable,
1913
- new Symbol(node.variable, 'variable', null, false, node.loc));
1914
- } catch (e) {
1915
- this.error(e.message);
1916
- }
1917
- for (const child of node.body) {
1918
- this.visitNode(child);
1919
- }
1920
- } finally {
1921
- this.currentScope = prevScope;
1922
- }
1923
- }
1924
-
1925
- visitJSXIf(node) {
1926
- this.visitExpression(node.condition);
1927
- for (const child of node.consequent) {
1928
- this.visitNode(child);
1929
- }
1930
- if (node.alternates) {
1931
- for (const alt of node.alternates) {
1932
- this.visitExpression(alt.condition);
1933
- for (const child of alt.body) {
1934
- this.visitNode(child);
1935
- }
1936
- }
1937
- }
1938
- if (node.alternate) {
1939
- for (const child of node.alternate) {
1940
- this.visitNode(child);
1941
- }
1942
- }
1943
- }
1970
+ // visitJSXElement, visitJSXFragment, visitJSXFor, visitJSXIf are in client-analyzer.js (lazy-loaded)
1944
1971
 
1945
1972
  // ─── New feature visitors ─────────────────────────────────
1946
1973
 
1947
1974
  visitBreakStatement(node) {
1948
1975
  if (!this._isInsideLoop()) {
1949
1976
  this.error("'break' can only be used inside a loop", node.loc);
1977
+ } else if (node.label && !this._isLabelInScope(node.label)) {
1978
+ this.error(`'break ${node.label}' references undefined label '${node.label}'`, node.loc);
1950
1979
  }
1951
1980
  }
1952
1981
 
1953
1982
  visitContinueStatement(node) {
1954
1983
  if (!this._isInsideLoop()) {
1955
1984
  this.error("'continue' can only be used inside a loop", node.loc);
1985
+ } else if (node.label && !this._isLabelInScope(node.label)) {
1986
+ this.error(`'continue ${node.label}' references undefined label '${node.label}'`, node.loc);
1956
1987
  }
1957
1988
  }
1958
1989
 
@@ -1963,7 +1994,8 @@ export class Analyzer {
1963
1994
  return true;
1964
1995
  case 'BlockStatement':
1965
1996
  if (node.body.length === 0) return false;
1966
- return this._definitelyReturns(node.body[node.body.length - 1]);
1997
+ // Any statement that definitely returns makes the block definitely return
1998
+ return node.body.some(stmt => this._definitelyReturns(stmt));
1967
1999
  case 'IfStatement':
1968
2000
  if (!node.elseBody) return false;
1969
2001
  const consequentReturns = this._definitelyReturns(node.consequent);
@@ -1971,8 +2003,9 @@ export class Analyzer {
1971
2003
  const allAlternatesReturn = (node.alternates || []).every(alt => this._definitelyReturns(alt.body));
1972
2004
  return consequentReturns && elseReturns && allAlternatesReturn;
1973
2005
  case 'GuardStatement':
1974
- // Guard's else block always runs if condition fails if it returns, the guard is a definite return path
1975
- return this._definitelyReturns(node.elseBody);
2006
+ // Guard only handles the failure casewhen condition is true, execution falls through
2007
+ // A guard alone never guarantees return on ALL paths
2008
+ return false;
1976
2009
  case 'MatchExpression': {
1977
2010
  const hasWildcard = node.arms.some(arm =>
1978
2011
  arm.pattern.type === 'WildcardPattern' ||
@@ -1983,15 +2016,15 @@ export class Analyzer {
1983
2016
  }
1984
2017
  case 'TryCatchStatement': {
1985
2018
  const tryReturns = node.tryBody.length > 0 &&
1986
- this._definitelyReturns(node.tryBody[node.tryBody.length - 1]);
2019
+ node.tryBody.some(s => this._definitelyReturns(s));
1987
2020
  const catchReturns = !node.catchBody || (node.catchBody.length > 0 &&
1988
- this._definitelyReturns(node.catchBody[node.catchBody.length - 1]));
2021
+ node.catchBody.some(s => this._definitelyReturns(s)));
1989
2022
  return tryReturns && catchReturns;
1990
2023
  }
1991
2024
  case 'ExpressionStatement':
1992
2025
  return this._definitelyReturns(node.expression);
1993
2026
  case 'CallExpression':
1994
- return true;
2027
+ return false;
1995
2028
  default:
1996
2029
  return false;
1997
2030
  }
@@ -2031,18 +2064,125 @@ export class Analyzer {
2031
2064
  const hasSpread = node.arguments.some(a => a.type === 'SpreadExpression');
2032
2065
  if (hasSpread) return;
2033
2066
 
2067
+ // Infer type parameter bindings from arguments for generic functions
2068
+ const typeParamBindings = new Map();
2069
+ if (fnSym._typeParams && fnSym._typeParams.length > 0) {
2070
+ for (let i = 0; i < node.arguments.length && i < fnSym._paramTypes.length; i++) {
2071
+ const arg = node.arguments[i];
2072
+ if (arg.type === 'NamedArgument' || arg.type === 'SpreadExpression') continue;
2073
+ const paramTypeAnn = fnSym._paramTypes[i];
2074
+ if (!paramTypeAnn) continue;
2075
+ const actualType = this._inferType(arg);
2076
+ if (actualType) {
2077
+ this._inferTypeParamBindings(paramTypeAnn, actualType, fnSym._typeParams, typeParamBindings);
2078
+ }
2079
+ }
2080
+ }
2081
+
2034
2082
  for (let i = 0; i < node.arguments.length && i < fnSym._paramTypes.length; i++) {
2035
2083
  const arg = node.arguments[i];
2036
2084
  if (arg.type === 'NamedArgument' || arg.type === 'SpreadExpression') continue;
2037
2085
  const paramTypeAnn = fnSym._paramTypes[i];
2038
2086
  if (!paramTypeAnn) continue;
2039
- const expectedType = this._typeAnnotationToString(paramTypeAnn);
2087
+ let expectedType = this._typeAnnotationToString(paramTypeAnn);
2088
+ // Substitute type parameters with inferred bindings
2089
+ if (typeParamBindings.size > 0) {
2090
+ expectedType = this._substituteTypeParams(expectedType, typeParamBindings);
2091
+ }
2092
+ // Skip check if expected type is still a bare type parameter (couldn't infer)
2093
+ if (fnSym._typeParams && fnSym._typeParams.includes(expectedType)) continue;
2040
2094
  const actualType = this._inferType(arg);
2041
2095
  if (!this._typesCompatible(expectedType, actualType)) {
2042
2096
  const paramName = fnSym._params ? fnSym._params[i] : `argument ${i + 1}`;
2043
- this.error(`Type mismatch: '${paramName}' expects ${expectedType}, but got ${actualType}`, arg.loc || node.loc);
2097
+ this.error(`Type mismatch: '${paramName}' expects ${expectedType}, but got ${actualType}`, arg.loc || node.loc, this._conversionHint(expectedType, actualType));
2098
+ }
2099
+ }
2100
+ }
2101
+
2102
+ /**
2103
+ * Infer type parameter bindings by matching a type annotation against an actual type string.
2104
+ * E.g., matching param annotation `T` against actual `Int` binds T=Int.
2105
+ * Matching `[T]` against `[Int]` binds T=Int.
2106
+ */
2107
+ _inferTypeParamBindings(ann, actualType, typeParams, bindings) {
2108
+ if (!ann || !actualType) return;
2109
+ const annStr = typeof ann === 'string' ? ann : (ann.type === 'TypeAnnotation' ? ann.name : null);
2110
+ if (!annStr) return;
2111
+
2112
+ // Direct type parameter match: T -> Int
2113
+ if (typeParams.includes(annStr) && !bindings.has(annStr)) {
2114
+ bindings.set(annStr, actualType);
2115
+ return;
2116
+ }
2117
+
2118
+ // Array type: [T] -> [Int]
2119
+ if (ann.type === 'ArrayTypeAnnotation' && actualType.startsWith('[') && actualType.endsWith(']')) {
2120
+ const innerActual = actualType.slice(1, -1);
2121
+ this._inferTypeParamBindings(ann.elementType, innerActual, typeParams, bindings);
2122
+ return;
2123
+ }
2124
+
2125
+ // Generic type: Result<T, E> -> Result<Int, String>
2126
+ if (ann.type === 'TypeAnnotation' && ann.typeParams && ann.typeParams.length > 0) {
2127
+ const parsed = this._parseGenericType(actualType);
2128
+ if (parsed.base === ann.name && parsed.params.length === ann.typeParams.length) {
2129
+ for (let i = 0; i < ann.typeParams.length; i++) {
2130
+ this._inferTypeParamBindings(ann.typeParams[i], parsed.params[i], typeParams, bindings);
2131
+ }
2132
+ }
2133
+ }
2134
+ }
2135
+
2136
+ /**
2137
+ * Substitute type parameter names in a type string with their inferred bindings.
2138
+ */
2139
+ _substituteTypeParams(typeStr, bindings) {
2140
+ if (!typeStr || bindings.size === 0) return typeStr;
2141
+ // Direct match
2142
+ if (bindings.has(typeStr)) return bindings.get(typeStr);
2143
+ // Array type
2144
+ if (typeStr.startsWith('[') && typeStr.endsWith(']')) {
2145
+ const inner = typeStr.slice(1, -1);
2146
+ return `[${this._substituteTypeParams(inner, bindings)}]`;
2147
+ }
2148
+ // Generic type
2149
+ const parsed = this._parseGenericType(typeStr);
2150
+ if (parsed.params.length > 0) {
2151
+ const substituted = parsed.params.map(p => this._substituteTypeParams(p, bindings));
2152
+ return `${parsed.base}<${substituted.join(', ')}>`;
2153
+ }
2154
+ return typeStr;
2155
+ }
2156
+
2157
+ /**
2158
+ * Resolve a type alias to its underlying type string.
2159
+ * E.g., if `type UserList = [User]`, resolves 'UserList' -> '[User]'.
2160
+ * For generic aliases like `type Callback<T> = fn(T) -> Result<T, String>`,
2161
+ * resolves 'Callback<Int>' by substituting T=Int.
2162
+ */
2163
+ _resolveTypeAlias(typeStr) {
2164
+ if (!typeStr) return typeStr;
2165
+ const parsed = this._parseGenericType(typeStr);
2166
+ const baseName = parsed.params.length > 0 ? parsed.base : typeStr;
2167
+
2168
+ // Look up the type in scope
2169
+ const sym = this.currentScope.lookup(baseName);
2170
+ if (!sym || sym.kind !== 'type' || !sym._typeAliasExpr) return typeStr;
2171
+
2172
+ // Resolve the alias
2173
+ let resolved = this._typeAnnotationToString(sym._typeAliasExpr);
2174
+ if (!resolved) return typeStr;
2175
+
2176
+ // Substitute type parameters if the alias is generic
2177
+ if (sym._typeParams && sym._typeParams.length > 0 && parsed.params.length > 0) {
2178
+ const bindings = new Map();
2179
+ for (let i = 0; i < sym._typeParams.length && i < parsed.params.length; i++) {
2180
+ bindings.set(sym._typeParams[i], parsed.params[i]);
2044
2181
  }
2182
+ resolved = this._substituteTypeParams(resolved, bindings);
2045
2183
  }
2184
+
2185
+ return resolved;
2046
2186
  }
2047
2187
 
2048
2188
  _checkBinaryExprTypes(node) {
@@ -2053,28 +2193,38 @@ export class Analyzer {
2053
2193
  if (op === '++') {
2054
2194
  // String concatenation: both sides should be String
2055
2195
  if (leftType && leftType !== 'String' && leftType !== 'Any') {
2056
- this.strictError(`Type mismatch: '++' expects String on left side, but got ${leftType}`, node.loc);
2196
+ this.strictError(`Type mismatch: '++' expects String on left side, but got ${leftType}`, node.loc, "try toString(value) to convert");
2057
2197
  }
2058
2198
  if (rightType && rightType !== 'String' && rightType !== 'Any') {
2059
- this.strictError(`Type mismatch: '++' expects String on right side, but got ${rightType}`, node.loc);
2199
+ this.strictError(`Type mismatch: '++' expects String on right side, but got ${rightType}`, node.loc, "try toString(value) to convert");
2060
2200
  }
2061
2201
  } else if (['-', '*', '/', '%', '**'].includes(op)) {
2202
+ // String literal * Int is valid (string repeat) — skip warning for that case
2203
+ if (op === '*') {
2204
+ const leftIsStr = node.left.type === 'StringLiteral' || node.left.type === 'TemplateLiteral';
2205
+ const rightIsStr = node.right.type === 'StringLiteral' || node.right.type === 'TemplateLiteral';
2206
+ if (leftIsStr || rightIsStr) return;
2207
+ }
2062
2208
  // Arithmetic: both sides must be numeric
2063
2209
  const numerics = new Set(['Int', 'Float']);
2064
2210
  if (leftType && !numerics.has(leftType) && leftType !== 'Any') {
2065
- this.strictError(`Type mismatch: '${op}' expects numeric type, but got ${leftType}`, node.loc);
2211
+ const hint = leftType === 'String' ? "try toInt(value) or toFloat(value) to parse" : null;
2212
+ this.strictError(`Type mismatch: '${op}' expects numeric type, but got ${leftType}`, node.loc, hint);
2066
2213
  }
2067
2214
  if (rightType && !numerics.has(rightType) && rightType !== 'Any') {
2068
- this.strictError(`Type mismatch: '${op}' expects numeric type, but got ${rightType}`, node.loc);
2215
+ const hint = rightType === 'String' ? "try toInt(value) or toFloat(value) to parse" : null;
2216
+ this.strictError(`Type mismatch: '${op}' expects numeric type, but got ${rightType}`, node.loc, hint);
2069
2217
  }
2070
2218
  } else if (op === '+') {
2071
2219
  // Addition: both sides must be numeric (Tova uses ++ for strings)
2072
2220
  const numerics = new Set(['Int', 'Float']);
2073
2221
  if (leftType && !numerics.has(leftType) && leftType !== 'Any') {
2074
- this.strictError(`Type mismatch: '+' expects numeric type, but got ${leftType}`, node.loc);
2222
+ const hint = leftType === 'String' ? "try toInt(value) or toFloat(value) to parse" : null;
2223
+ this.strictError(`Type mismatch: '+' expects numeric type, but got ${leftType}`, node.loc, hint);
2075
2224
  }
2076
2225
  if (rightType && !numerics.has(rightType) && rightType !== 'Any') {
2077
- this.strictError(`Type mismatch: '+' expects numeric type, but got ${rightType}`, node.loc);
2226
+ const hint = rightType === 'String' ? "try toInt(value) or toFloat(value) to parse" : null;
2227
+ this.strictError(`Type mismatch: '+' expects numeric type, but got ${rightType}`, node.loc, hint);
2078
2228
  }
2079
2229
  }
2080
2230
  }
@@ -2111,7 +2261,7 @@ export class Analyzer {
2111
2261
  }
2112
2262
  if (crossedBoundary) {
2113
2263
  const sym = scope.symbols.get(name);
2114
- if (sym) return true;
2264
+ if (sym && sym.kind !== 'builtin') return true;
2115
2265
  }
2116
2266
  scope = scope.parent;
2117
2267
  }
@@ -2138,9 +2288,36 @@ export class Analyzer {
2138
2288
  return false;
2139
2289
  }
2140
2290
 
2291
+ _isLabelInScope(label) {
2292
+ // Walk up scopes to find a matching loop label
2293
+ let scope = this.currentScope;
2294
+ while (scope) {
2295
+ if (scope._isLoop && scope._loopLabel === label) return true;
2296
+ if (scope.context === 'function') return false;
2297
+ scope = scope.parent;
2298
+ }
2299
+ return false;
2300
+ }
2301
+
2141
2302
  visitGuardStatement(node) {
2142
2303
  this.visitExpression(node.condition);
2143
2304
  this.visitNode(node.elseBody);
2305
+
2306
+ // Type narrowing after guard: guard x != nil else { return }
2307
+ // The condition being true means x is non-nil for the rest of the scope
2308
+ const narrowing = this._extractNarrowingInfo(node.condition);
2309
+ if (narrowing) {
2310
+ const sym = this.currentScope.lookup(narrowing.varName);
2311
+ if (sym) {
2312
+ // Narrow the variable in the current scope (not a new child scope)
2313
+ const narrowedSym = new Symbol(narrowing.varName, sym.kind, null, sym.mutable, sym.loc);
2314
+ narrowedSym.inferredType = narrowing.narrowedType;
2315
+ narrowedSym._narrowed = true;
2316
+ narrowedSym.used = sym.used;
2317
+ // Replace in current scope so the rest of the function sees the narrowed type
2318
+ this.currentScope.symbols.set(narrowing.varName, narrowedSym);
2319
+ }
2320
+ }
2144
2321
  }
2145
2322
 
2146
2323
  visitInterfaceDeclaration(node) {
@@ -2194,16 +2371,16 @@ export class Analyzer {
2194
2371
  for (const required of traitSym._interfaceMethods) {
2195
2372
  const provided = providedMethods.get(required.name);
2196
2373
  if (!provided) {
2197
- this.warn(`Impl for '${typeName || 'type'}' missing required method '${required.name}' from trait '${node.traitName}'`, node.loc);
2374
+ this.warn(`Impl for '${typeName || 'type'}' missing required method '${required.name}' from trait '${node.traitName}'`, node.loc, null, { code: 'W300' });
2198
2375
  } else {
2199
2376
  // Check parameter count matches (excluding self)
2200
2377
  if (required.paramCount > 0 && provided.paramCount !== required.paramCount) {
2201
- this.warn(`Method '${required.name}' in impl for '${typeName}' has ${provided.paramCount} parameters, but trait '${node.traitName}' expects ${required.paramCount}`, node.loc);
2378
+ this.warn(`Method '${required.name}' in impl for '${typeName}' has ${provided.paramCount} parameters, but trait '${node.traitName}' expects ${required.paramCount}`, node.loc, null, { code: 'W301' });
2202
2379
  }
2203
2380
  // Check return type matches if both are annotated
2204
2381
  if (required.returnType && provided.returnType) {
2205
2382
  if (!provided.returnType.isAssignableTo(required.returnType)) {
2206
- this.warn(`Method '${required.name}' return type mismatch in impl for '${typeName}': expected ${required.returnType}, got ${provided.returnType}`, node.loc);
2383
+ this.warn(`Method '${required.name}' return type mismatch in impl for '${typeName}': expected ${required.returnType}, got ${provided.returnType}`, node.loc, null, { code: 'W302' });
2207
2384
  }
2208
2385
  }
2209
2386
  }
@@ -2277,8 +2454,18 @@ export class Analyzer {
2277
2454
 
2278
2455
  visitTypeAlias(node) {
2279
2456
  try {
2280
- this.currentScope.define(node.name,
2281
- new Symbol(node.name, 'type', null, false, node.loc));
2457
+ const typeSym = new Symbol(node.name, 'type', null, false, node.loc);
2458
+ // Store type alias info for resolution
2459
+ if (node.typeExpr) {
2460
+ typeSym._typeAliasExpr = node.typeExpr;
2461
+ typeSym._typeParams = node.typeParams || [];
2462
+ const resolved = typeAnnotationToType(node.typeExpr);
2463
+ if (resolved) {
2464
+ typeSym._typeStructure = resolved;
2465
+ this.typeRegistry.types.set(node.name, resolved);
2466
+ }
2467
+ }
2468
+ this.currentScope.define(node.name, typeSym);
2282
2469
  } catch (e) {
2283
2470
  this.error(e.message);
2284
2471
  }
@@ -2296,7 +2483,7 @@ export class Analyzer {
2296
2483
  scope = scope.parent;
2297
2484
  }
2298
2485
  if (!insideFunction) {
2299
- this.warn("'defer' used outside of a function", node.loc);
2486
+ this.warn("'defer' used outside of a function", node.loc, null, { code: 'W208' });
2300
2487
  }
2301
2488
  if (node.body) {
2302
2489
  if (node.body.type === 'BlockStatement') {