tova 0.1.1 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tova",
3
- "version": "0.1.1",
3
+ "version": "0.2.2",
4
4
  "description": "Tova — a modern programming language that transpiles to JavaScript, unifying frontend and backend",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -33,6 +33,12 @@
33
33
  "bugs": {
34
34
  "url": "https://github.com/tova-lang/tova-lang/issues"
35
35
  },
36
- "keywords": ["language", "transpiler", "fullstack", "javascript"],
36
+ "author": "Enoch Kujem Abassey",
37
+ "keywords": [
38
+ "language",
39
+ "transpiler",
40
+ "fullstack",
41
+ "javascript"
42
+ ],
37
43
  "license": "MIT"
38
44
  }
@@ -1,5 +1,38 @@
1
1
  import { Scope, Symbol } from './scope.js';
2
2
  import { PIPE_TARGET } from '../parser/ast.js';
3
+ import { BUILTIN_NAMES } from '../stdlib/inline.js';
4
+ import {
5
+ Type, PrimitiveType, NilType, AnyType, UnknownType,
6
+ ArrayType, TupleType, FunctionType, RecordType, ADTType,
7
+ GenericType, TypeVariable, UnionType,
8
+ typeAnnotationToType, typeFromString, typesCompatible,
9
+ isNumericType, isFloatNarrowing,
10
+ } from './types.js';
11
+
12
+ const _JS_GLOBALS = new Set([
13
+ 'console', 'document', 'window', 'globalThis', 'self',
14
+ 'JSON', 'Math', 'Date', 'RegExp', 'Error', 'TypeError', 'RangeError',
15
+ 'Promise', 'Set', 'Map', 'WeakSet', 'WeakMap', 'Symbol',
16
+ 'Array', 'Object', 'String', 'Number', 'Boolean', 'Function',
17
+ 'parseInt', 'parseFloat', 'isNaN', 'isFinite', 'NaN', 'Infinity',
18
+ 'undefined', 'null', 'true', 'false',
19
+ 'setTimeout', 'setInterval', 'clearTimeout', 'clearInterval',
20
+ 'queueMicrotask', 'structuredClone',
21
+ 'URL', 'URLSearchParams', 'Headers', 'Request', 'Response',
22
+ 'FormData', 'Blob', 'File', 'FileReader',
23
+ 'AbortController', 'AbortSignal',
24
+ 'TextEncoder', 'TextDecoder',
25
+ 'crypto', 'performance', 'navigator', 'location', 'history',
26
+ 'localStorage', 'sessionStorage',
27
+ 'fetch', 'alert', 'confirm', 'prompt',
28
+ 'Bun', 'Deno', 'process', 'require', 'module', 'exports', '__dirname', '__filename',
29
+ 'Buffer', 'atob', 'btoa',
30
+ ]);
31
+
32
+ const _TOVA_RUNTIME = new Set([
33
+ 'Ok', 'Err', 'Some', 'None', 'Result', 'Option',
34
+ 'db', 'server', 'client', 'shared',
35
+ ]);
3
36
 
4
37
  export class Analyzer {
5
38
  constructor(ast, filename = '<stdin>', options = {}) {
@@ -8,12 +41,20 @@ export class Analyzer {
8
41
  this.errors = [];
9
42
  this.warnings = [];
10
43
  this.tolerant = options.tolerant || false;
44
+ this.strict = options.strict || false;
11
45
  this.globalScope = new Scope(null, 'module');
12
46
  this.currentScope = this.globalScope;
13
47
  this._allScopes = []; // Track all scopes for unused variable checking
14
48
  this._functionReturnTypeStack = []; // Stack of expected return types for type checking
15
49
  this._asyncDepth = 0; // Track nesting inside async functions for await validation
16
50
 
51
+ // Type registry for LSP
52
+ this.typeRegistry = {
53
+ types: new Map(), // type name → ADTType | RecordType
54
+ impls: new Map(), // type name → [{ name, params, returnType }]
55
+ traits: new Map(), // trait name → [{ name, paramTypes, returnType }]
56
+ };
57
+
17
58
  // Register built-in types
18
59
  this.registerBuiltins();
19
60
  }
@@ -43,6 +84,47 @@ export class Analyzer {
43
84
  'snake_case', 'camel_case',
44
85
  // Math extras
45
86
  'min', 'max',
87
+ // Table operations
88
+ 'Table', 'table_where', 'table_select', 'table_derive',
89
+ 'table_group_by', 'table_agg', 'table_sort_by', 'table_limit',
90
+ 'table_join', 'table_pivot', 'table_unpivot', 'table_explode',
91
+ 'table_union', 'table_drop_duplicates', 'table_rename',
92
+ // Table aggregation helpers
93
+ 'agg_sum', 'agg_count', 'agg_mean', 'agg_median', 'agg_min', 'agg_max',
94
+ // Data exploration
95
+ 'peek', 'describe', 'schema_of',
96
+ // Data cleaning
97
+ 'cast', 'drop_nil', 'fill_nil', 'filter_ok', 'filter_err',
98
+ // I/O
99
+ 'read', 'write', 'stream',
100
+ // CSV/JSONL helpers
101
+ '__parseCSV', '__parseJSONL',
102
+ // Table operation aliases (short names)
103
+ 'where', 'select', 'derive', 'agg', 'sort_by', 'limit',
104
+ 'pivot', 'unpivot', 'explode', 'union', 'drop_duplicates', 'rename',
105
+ 'mean', 'median',
106
+ // Strings (new)
107
+ 'index_of', 'last_index_of', 'count_of', 'reverse_str', 'substr',
108
+ 'is_empty', 'kebab_case', 'center',
109
+ // Collections (new)
110
+ 'zip_with', 'frequencies', 'scan', 'min_by', 'max_by', 'sum_by',
111
+ 'product', 'from_entries', 'has_key', 'get', 'pick', 'omit',
112
+ 'map_values', 'sliding_window',
113
+ // JSON
114
+ 'json_parse', 'json_stringify', 'json_pretty',
115
+ // Functional
116
+ 'compose', 'pipe_fn', 'identity', 'memoize', 'debounce', 'throttle',
117
+ 'once', 'negate',
118
+ // Error handling
119
+ 'try_fn', 'try_async',
120
+ // Async
121
+ 'parallel', 'timeout', 'retry',
122
+ // Encoding
123
+ 'base64_encode', 'base64_decode', 'url_encode', 'url_decode',
124
+ // Math (new)
125
+ 'hypot', 'lerp', 'divmod', 'avg',
126
+ // Date/Time
127
+ 'now', 'now_iso',
46
128
  ];
47
129
  for (const name of builtins) {
48
130
  this.globalScope.define(name, new Symbol(name, 'builtin', null, false, { line: 0, column: 0, file: '<builtin>' }));
@@ -69,6 +151,14 @@ export class Analyzer {
69
151
  });
70
152
  }
71
153
 
154
+ strictError(message, loc) {
155
+ if (this.strict) {
156
+ this.error(message, loc);
157
+ } else {
158
+ this.warn(message, loc);
159
+ }
160
+ }
161
+
72
162
  analyze() {
73
163
  // Pre-pass: collect named server block functions for inter-server RPC validation
74
164
  this.serverBlockFunctions = new Map(); // blockName -> [functionName, ...]
@@ -102,7 +192,7 @@ export class Analyzer {
102
192
 
103
193
  if (this.errors.length > 0) {
104
194
  if (this.tolerant) {
105
- return { warnings: this.warnings, errors: this.errors, scope: this.globalScope };
195
+ return { warnings: this.warnings, errors: this.errors, scope: this.globalScope, typeRegistry: this.typeRegistry };
106
196
  }
107
197
  const msgs = this.errors.map(e => ` ${e.file}:${e.line}:${e.column} — ${e.message}`);
108
198
  const err = new Error(`Analysis errors:\n${msgs.join('\n')}`);
@@ -111,7 +201,7 @@ export class Analyzer {
111
201
  throw err;
112
202
  }
113
203
 
114
- return { warnings: this.warnings, scope: this.globalScope };
204
+ return { warnings: this.warnings, scope: this.globalScope, typeRegistry: this.typeRegistry };
115
205
  }
116
206
 
117
207
  _checkUnusedSymbols() {
@@ -377,6 +467,13 @@ export class Analyzer {
377
467
  case 'CacheDeclaration': return this.visitCacheDeclaration(node);
378
468
  case 'SseDeclaration': return this.visitSseDeclaration(node);
379
469
  case 'ModelDeclaration': return this.visitModelDeclaration(node);
470
+ case 'AiConfigDeclaration': return; // handled at block level
471
+ case 'DataBlock': return this.visitDataBlock(node);
472
+ case 'SourceDeclaration': return;
473
+ case 'PipelineDeclaration': return;
474
+ case 'ValidateBlock': return;
475
+ case 'RefreshPolicy': return;
476
+ case 'RefinementType': return;
380
477
  case 'TestBlock': return this.visitTestBlock(node);
381
478
  case 'ComponentStyleBlock': return; // raw CSS — no analysis needed
382
479
  case 'ImplDeclaration': return this.visitImplDeclaration(node);
@@ -517,6 +614,14 @@ export class Analyzer {
517
614
  return;
518
615
  case 'JSXElement':
519
616
  return this.visitJSXElement(node);
617
+ // Column expressions (for table operations) — no semantic analysis needed
618
+ case 'ColumnExpression':
619
+ return;
620
+ case 'ColumnAssignment':
621
+ this.visitExpression(node.expression);
622
+ return;
623
+ case 'NegatedColumnExpression':
624
+ return;
520
625
  }
521
626
  }
522
627
 
@@ -543,6 +648,19 @@ export class Analyzer {
543
648
  }
544
649
  }
545
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
+
546
664
  for (const stmt of node.body) {
547
665
  this.visitNode(stmt);
548
666
  }
@@ -552,6 +670,25 @@ export class Analyzer {
552
670
  }
553
671
  }
554
672
 
673
+ visitDataBlock(node) {
674
+ // Register source and pipeline names in global scope
675
+ for (const stmt of node.body) {
676
+ if (stmt.type === 'SourceDeclaration') {
677
+ try {
678
+ this.currentScope.define(stmt.name,
679
+ new Symbol(stmt.name, 'variable', null, false, stmt.loc));
680
+ } catch (e) { /* already defined */ }
681
+ if (stmt.expression) this.visitExpression(stmt.expression);
682
+ } else if (stmt.type === 'PipelineDeclaration') {
683
+ try {
684
+ this.currentScope.define(stmt.name,
685
+ new Symbol(stmt.name, 'variable', null, false, stmt.loc));
686
+ } catch (e) { /* already defined */ }
687
+ if (stmt.expression) this.visitExpression(stmt.expression);
688
+ }
689
+ }
690
+ }
691
+
555
692
  visitClientBlock(node) {
556
693
  const prevScope = this.currentScope;
557
694
  this.currentScope = this.currentScope.child('client');
@@ -596,7 +733,11 @@ export class Analyzer {
596
733
  if (existing.inferredType && i < node.values.length) {
597
734
  const newType = this._inferType(node.values[i]);
598
735
  if (!this._typesCompatible(existing.inferredType, newType)) {
599
- this.warn(`Type mismatch: '${target}' is ${existing.inferredType}, but assigned ${newType}`, node.loc);
736
+ this.strictError(`Type mismatch: '${target}' is ${existing.inferredType}, but assigned ${newType}`, node.loc);
737
+ }
738
+ // Float narrowing warning in strict mode
739
+ if (this.strict && newType === 'Float' && existing.inferredType === 'Int') {
740
+ this.warn(`Potential data loss: assigning Float to Int variable '${target}'`, node.loc);
600
741
  }
601
742
  }
602
743
  existing.used = true;
@@ -676,6 +817,9 @@ export class Analyzer {
676
817
 
677
818
  const prevScope = this.currentScope;
678
819
  this.currentScope = this.currentScope.child('function');
820
+ if (node.loc) {
821
+ this.currentScope.startLoc = { line: node.loc.line, column: node.loc.column };
822
+ }
679
823
 
680
824
  // Push expected return type for return-statement checking
681
825
  const expectedReturn = node.returnType ? this._typeAnnotationToString(node.returnType) : null;
@@ -761,10 +905,28 @@ export class Analyzer {
761
905
  }
762
906
 
763
907
  visitTypeDeclaration(node) {
908
+ // Build ADT type structure
909
+ const variants = new Map();
910
+ for (const variant of node.variants) {
911
+ if (variant.type === 'TypeVariant') {
912
+ const fields = new Map();
913
+ for (const f of variant.fields) {
914
+ const fieldType = f.typeAnnotation ? typeAnnotationToType(f.typeAnnotation) : Type.ANY;
915
+ fields.set(f.name, fieldType || Type.ANY);
916
+ }
917
+ variants.set(variant.name, fields);
918
+ }
919
+ }
920
+ const adtType = new ADTType(node.name, node.typeParams || [], variants);
921
+
764
922
  try {
765
923
  const typeSym = new Symbol(node.name, 'type', null, false, node.loc);
766
924
  typeSym._typeParams = node.typeParams || [];
925
+ typeSym._typeStructure = adtType;
767
926
  this.currentScope.define(node.name, typeSym);
927
+
928
+ // Register in type registry for LSP
929
+ this.typeRegistry.types.set(node.name, adtType);
768
930
  } catch (e) {
769
931
  this.error(e.message);
770
932
  }
@@ -821,11 +983,17 @@ export class Analyzer {
821
983
  visitBlock(node) {
822
984
  const prevScope = this.currentScope;
823
985
  this.currentScope = this.currentScope.child('block');
986
+ if (node.loc) {
987
+ this.currentScope.startLoc = { line: node.loc.line, column: node.loc.column };
988
+ }
824
989
  try {
825
990
  for (const stmt of node.body) {
826
991
  this.visitNode(stmt);
827
992
  }
828
993
  } finally {
994
+ if (node.loc) {
995
+ this.currentScope.endLoc = { line: node.endLoc?.line || node.loc.line + 100, column: node.endLoc?.column || 0 };
996
+ }
829
997
  this.currentScope = prevScope;
830
998
  }
831
999
  }
@@ -949,23 +1117,23 @@ export class Analyzer {
949
1117
  const numerics = new Set(['Int', 'Float']);
950
1118
  if (['-=', '*=', '/='].includes(op)) {
951
1119
  if (!numerics.has(sym.inferredType) && sym.inferredType !== 'Any') {
952
- this.warn(`Type mismatch: '${op}' requires numeric type, but '${node.target.name}' is ${sym.inferredType}`, node.loc);
1120
+ this.strictError(`Type mismatch: '${op}' requires numeric type, but '${node.target.name}' is ${sym.inferredType}`, node.loc);
953
1121
  }
954
1122
  const valType = this._inferType(node.value);
955
1123
  if (valType && !numerics.has(valType) && valType !== 'Any') {
956
- this.warn(`Type mismatch: '${op}' requires numeric value, but got ${valType}`, node.loc);
1124
+ this.strictError(`Type mismatch: '${op}' requires numeric value, but got ${valType}`, node.loc);
957
1125
  }
958
1126
  } else if (op === '+=') {
959
1127
  // += on numerics requires numeric value, on strings requires string
960
1128
  if (numerics.has(sym.inferredType)) {
961
1129
  const valType = this._inferType(node.value);
962
1130
  if (valType && !numerics.has(valType) && valType !== 'Any') {
963
- this.warn(`Type mismatch: '${op}' on numeric variable requires numeric value, but got ${valType}`, node.loc);
1131
+ this.strictError(`Type mismatch: '${op}' on numeric variable requires numeric value, but got ${valType}`, node.loc);
964
1132
  }
965
1133
  } else if (sym.inferredType === 'String') {
966
1134
  const valType = this._inferType(node.value);
967
1135
  if (valType && valType !== 'String' && valType !== 'Any') {
968
- this.warn(`Type mismatch: '${op}' on String variable requires String value, but got ${valType}`, node.loc);
1136
+ this.strictError(`Type mismatch: '${op}' on String variable requires String value, but got ${valType}`, node.loc);
969
1137
  }
970
1138
  }
971
1139
  }
@@ -1447,44 +1615,14 @@ export class Analyzer {
1447
1615
  }
1448
1616
 
1449
1617
  _isKnownGlobal(name) {
1450
- const jsGlobals = new Set([
1451
- // JS built-ins
1452
- 'console', 'document', 'window', 'globalThis', 'self',
1453
- 'JSON', 'Math', 'Date', 'RegExp', 'Error', 'TypeError', 'RangeError',
1454
- 'Promise', 'Set', 'Map', 'WeakSet', 'WeakMap', 'Symbol',
1455
- 'Array', 'Object', 'String', 'Number', 'Boolean', 'Function',
1456
- 'parseInt', 'parseFloat', 'isNaN', 'isFinite', 'NaN', 'Infinity',
1457
- 'undefined', 'null', 'true', 'false',
1458
- 'setTimeout', 'setInterval', 'clearTimeout', 'clearInterval',
1459
- 'queueMicrotask', 'structuredClone',
1460
- 'URL', 'URLSearchParams', 'Headers', 'Request', 'Response',
1461
- 'FormData', 'Blob', 'File', 'FileReader',
1462
- 'AbortController', 'AbortSignal',
1463
- 'TextEncoder', 'TextDecoder',
1464
- 'crypto', 'performance', 'navigator', 'location', 'history',
1465
- 'localStorage', 'sessionStorage',
1466
- 'fetch', 'alert', 'confirm', 'prompt',
1467
- 'Bun', 'Deno', 'process', 'require', 'module', 'exports', '__dirname', '__filename',
1468
- 'Buffer', 'atob', 'btoa',
1469
- // Tova runtime
1470
- 'print', 'range', 'len', 'type_of', 'enumerate', 'zip',
1471
- 'map', 'filter', 'reduce', 'sum', 'sorted', 'reversed',
1472
- 'Ok', 'Err', 'Some', 'None', 'Result', 'Option',
1473
- 'db', 'server', 'client', 'shared',
1474
- // Tova stdlib — collections
1475
- 'find', 'any', 'all', 'flat_map', 'unique', 'group_by',
1476
- 'chunk', 'flatten', 'take', 'drop', 'first', 'last',
1477
- 'count', 'partition',
1478
- // Tova stdlib — math
1479
- 'abs', 'floor', 'ceil', 'round', 'clamp', 'sqrt', 'pow', 'random',
1480
- // Tova stdlib — strings
1481
- 'trim', 'split', 'join', 'replace', 'repeat',
1482
- // Tova stdlib — utility
1483
- 'keys', 'values', 'entries', 'merge', 'freeze', 'clone',
1484
- // Tova stdlib — async
1485
- 'sleep',
1486
- ]);
1487
- return jsGlobals.has(name);
1618
+ // Tova stdlib (auto-synced from BUILTIN_FUNCTIONS in inline.js)
1619
+ if (BUILTIN_NAMES.has(name)) return true;
1620
+
1621
+ // Tova runtime names
1622
+ if (_TOVA_RUNTIME.has(name)) return true;
1623
+
1624
+ // JS globals / platform APIs
1625
+ return _JS_GLOBALS.has(name);
1488
1626
  }
1489
1627
 
1490
1628
  visitLambda(node) {
@@ -1554,7 +1692,29 @@ export class Analyzer {
1554
1692
  );
1555
1693
  if (hasWildcard) return; // Catch-all exists, always exhaustive
1556
1694
 
1557
- // If subject is an identifier, try to find its type for variant checking
1695
+ // Try to resolve the subject type for better checking
1696
+ let subjectType = null;
1697
+ if (node.subject) {
1698
+ const subjectTypeStr = this._inferType(node.subject);
1699
+ if (subjectTypeStr) {
1700
+ // Look up type structure from type registry
1701
+ const typeStructure = this.typeRegistry.types.get(subjectTypeStr);
1702
+ if (typeStructure instanceof ADTType) {
1703
+ subjectType = typeStructure;
1704
+ }
1705
+ }
1706
+ // Also try to find type from identifier
1707
+ if (!subjectType && node.subject.type === 'Identifier') {
1708
+ const sym = this.currentScope.lookup(node.subject.name);
1709
+ if (sym && sym.inferredType) {
1710
+ const typeStructure = this.typeRegistry.types.get(sym.inferredType);
1711
+ if (typeStructure instanceof ADTType) {
1712
+ subjectType = typeStructure;
1713
+ }
1714
+ }
1715
+ }
1716
+ }
1717
+
1558
1718
  const variantNames = new Set();
1559
1719
  const coveredVariants = new Set();
1560
1720
 
@@ -1567,6 +1727,17 @@ export class Analyzer {
1567
1727
 
1568
1728
  // If we have variant patterns, check if all known variants are covered
1569
1729
  if (coveredVariants.size > 0) {
1730
+ // If we have the ADT type structure, use it for precise checking
1731
+ if (subjectType) {
1732
+ const allVariants = subjectType.getVariantNames();
1733
+ for (const v of allVariants) {
1734
+ if (!coveredVariants.has(v)) {
1735
+ this.warn(`Non-exhaustive match: missing '${v}' variant from type '${subjectType.name}'`, node.loc);
1736
+ }
1737
+ }
1738
+ return; // Done — used precise ADT checking
1739
+ }
1740
+
1570
1741
  // Check built-in Result/Option types
1571
1742
  if (coveredVariants.has('Ok') || coveredVariants.has('Err')) {
1572
1743
  if (!coveredVariants.has('Ok')) {
@@ -1829,9 +2000,9 @@ export class Analyzer {
1829
2000
  const name = node.callee.name;
1830
2001
 
1831
2002
  if (actualCount > fnSym._totalParamCount) {
1832
- this.warn(`'${name}' expects ${fnSym._totalParamCount} argument${fnSym._totalParamCount !== 1 ? 's' : ''}, but got ${actualCount}`, node.loc);
2003
+ this.strictError(`'${name}' expects ${fnSym._totalParamCount} argument${fnSym._totalParamCount !== 1 ? 's' : ''}, but got ${actualCount}`, node.loc);
1833
2004
  } else if (actualCount < fnSym._requiredParamCount) {
1834
- this.warn(`'${name}' expects at least ${fnSym._requiredParamCount} argument${fnSym._requiredParamCount !== 1 ? 's' : ''}, but got ${actualCount}`, node.loc);
2005
+ this.strictError(`'${name}' expects at least ${fnSym._requiredParamCount} argument${fnSym._requiredParamCount !== 1 ? 's' : ''}, but got ${actualCount}`, node.loc);
1835
2006
  }
1836
2007
  }
1837
2008
 
@@ -1865,28 +2036,28 @@ export class Analyzer {
1865
2036
  if (op === '++') {
1866
2037
  // String concatenation: both sides should be String
1867
2038
  if (leftType && leftType !== 'String' && leftType !== 'Any') {
1868
- this.warn(`Type mismatch: '++' expects String on left side, but got ${leftType}`, node.loc);
2039
+ this.strictError(`Type mismatch: '++' expects String on left side, but got ${leftType}`, node.loc);
1869
2040
  }
1870
2041
  if (rightType && rightType !== 'String' && rightType !== 'Any') {
1871
- this.warn(`Type mismatch: '++' expects String on right side, but got ${rightType}`, node.loc);
2042
+ this.strictError(`Type mismatch: '++' expects String on right side, but got ${rightType}`, node.loc);
1872
2043
  }
1873
2044
  } else if (['-', '*', '/', '%', '**'].includes(op)) {
1874
2045
  // Arithmetic: both sides must be numeric
1875
2046
  const numerics = new Set(['Int', 'Float']);
1876
2047
  if (leftType && !numerics.has(leftType) && leftType !== 'Any') {
1877
- this.warn(`Type mismatch: '${op}' expects numeric type, but got ${leftType}`, node.loc);
2048
+ this.strictError(`Type mismatch: '${op}' expects numeric type, but got ${leftType}`, node.loc);
1878
2049
  }
1879
2050
  if (rightType && !numerics.has(rightType) && rightType !== 'Any') {
1880
- this.warn(`Type mismatch: '${op}' expects numeric type, but got ${rightType}`, node.loc);
2051
+ this.strictError(`Type mismatch: '${op}' expects numeric type, but got ${rightType}`, node.loc);
1881
2052
  }
1882
2053
  } else if (op === '+') {
1883
2054
  // Addition: both sides must be numeric (Tova uses ++ for strings)
1884
2055
  const numerics = new Set(['Int', 'Float']);
1885
2056
  if (leftType && !numerics.has(leftType) && leftType !== 'Any') {
1886
- this.warn(`Type mismatch: '+' expects numeric type, but got ${leftType}`, node.loc);
2057
+ this.strictError(`Type mismatch: '+' expects numeric type, but got ${leftType}`, node.loc);
1887
2058
  }
1888
2059
  if (rightType && !numerics.has(rightType) && rightType !== 'Any') {
1889
- this.warn(`Type mismatch: '+' expects numeric type, but got ${rightType}`, node.loc);
2060
+ this.strictError(`Type mismatch: '+' expects numeric type, but got ${rightType}`, node.loc);
1890
2061
  }
1891
2062
  }
1892
2063
  }
@@ -1957,14 +2128,72 @@ export class Analyzer {
1957
2128
 
1958
2129
  visitInterfaceDeclaration(node) {
1959
2130
  try {
1960
- this.currentScope.define(node.name,
1961
- new Symbol(node.name, 'type', null, false, node.loc));
2131
+ const sym = new Symbol(node.name, 'type', null, false, node.loc);
2132
+ // Store method signatures for conformance checking
2133
+ sym._interfaceMethods = (node.methods || []).map(m => ({
2134
+ name: m.name,
2135
+ paramTypes: (m.params || []).map(p => typeAnnotationToType(p.typeAnnotation)),
2136
+ returnType: typeAnnotationToType(m.returnType),
2137
+ paramCount: (m.params || []).length,
2138
+ }));
2139
+ this.currentScope.define(node.name, sym);
2140
+
2141
+ // Register in type registry for LSP
2142
+ this.typeRegistry.traits.set(node.name, sym._interfaceMethods);
1962
2143
  } catch (e) {
1963
2144
  this.error(e.message);
1964
2145
  }
1965
2146
  }
1966
2147
 
1967
2148
  visitImplDeclaration(node) {
2149
+ // Collect provided method names for conformance checking
2150
+ const providedMethods = new Map();
2151
+ for (const method of node.methods) {
2152
+ providedMethods.set(method.name, {
2153
+ paramCount: (method.params || []).filter(p => p.name !== 'self').length,
2154
+ returnType: method.returnType ? typeAnnotationToType(method.returnType) : null,
2155
+ });
2156
+ }
2157
+
2158
+ // Register impl methods in type registry for LSP
2159
+ const typeName = node.typeName || node.target;
2160
+ if (typeName) {
2161
+ const existingImpls = this.typeRegistry.impls.get(typeName) || [];
2162
+ for (const method of node.methods) {
2163
+ existingImpls.push({
2164
+ name: method.name,
2165
+ params: (method.params || []).map(p => p.name),
2166
+ paramTypes: (method.params || []).map(p => typeAnnotationToType(p.typeAnnotation)),
2167
+ returnType: typeAnnotationToType(method.returnType),
2168
+ });
2169
+ }
2170
+ this.typeRegistry.impls.set(typeName, existingImpls);
2171
+ }
2172
+
2173
+ // Trait/interface conformance checking
2174
+ if (node.traitName) {
2175
+ const traitSym = this.currentScope.lookup(node.traitName);
2176
+ if (traitSym && traitSym._interfaceMethods) {
2177
+ for (const required of traitSym._interfaceMethods) {
2178
+ const provided = providedMethods.get(required.name);
2179
+ if (!provided) {
2180
+ this.warn(`Impl for '${typeName || 'type'}' missing required method '${required.name}' from trait '${node.traitName}'`, node.loc);
2181
+ } else {
2182
+ // Check parameter count matches (excluding self)
2183
+ if (required.paramCount > 0 && provided.paramCount !== required.paramCount) {
2184
+ this.warn(`Method '${required.name}' in impl for '${typeName}' has ${provided.paramCount} parameters, but trait '${node.traitName}' expects ${required.paramCount}`, node.loc);
2185
+ }
2186
+ // Check return type matches if both are annotated
2187
+ if (required.returnType && provided.returnType) {
2188
+ if (!provided.returnType.isAssignableTo(required.returnType)) {
2189
+ this.warn(`Method '${required.name}' return type mismatch in impl for '${typeName}': expected ${required.returnType}, got ${provided.returnType}`, node.loc);
2190
+ }
2191
+ }
2192
+ }
2193
+ }
2194
+ }
2195
+ }
2196
+
1968
2197
  // Validate that methods reference the type
1969
2198
  for (const method of node.methods) {
1970
2199
  this.pushScope('function');
@@ -1993,8 +2222,18 @@ export class Analyzer {
1993
2222
 
1994
2223
  visitTraitDeclaration(node) {
1995
2224
  try {
1996
- this.currentScope.define(node.name,
1997
- new Symbol(node.name, 'type', null, false, node.loc));
2225
+ const sym = new Symbol(node.name, 'type', null, false, node.loc);
2226
+ // Store method signatures for conformance checking
2227
+ sym._interfaceMethods = (node.methods || []).map(m => ({
2228
+ name: m.name,
2229
+ paramTypes: (m.params || []).map(p => typeAnnotationToType(p.typeAnnotation)),
2230
+ returnType: typeAnnotationToType(m.returnType),
2231
+ paramCount: (m.params || []).filter(p => p.name !== 'self').length,
2232
+ }));
2233
+ this.currentScope.define(node.name, sym);
2234
+
2235
+ // Register in type registry for LSP
2236
+ this.typeRegistry.traits.set(node.name, sym._interfaceMethods);
1998
2237
  } catch (e) {
1999
2238
  this.error(e.message);
2000
2239
  }
@@ -4,10 +4,11 @@ export class Symbol {
4
4
  constructor(name, kind, type, mutable, loc) {
5
5
  this.name = name;
6
6
  this.kind = kind; // 'variable', 'function', 'type', 'parameter', 'state', 'computed', 'component'
7
- this.type = type; // type annotation (optional)
7
+ this.type = type; // Type object or raw type annotation (optional)
8
8
  this.mutable = mutable; // true for 'var' declarations
9
9
  this.loc = loc;
10
10
  this.used = false;
11
+ this.declaredType = null; // raw annotation for display purposes
11
12
  }
12
13
  }
13
14
 
@@ -17,10 +18,18 @@ export class Scope {
17
18
  this.context = context; // 'module', 'server', 'client', 'shared', 'function', 'block'
18
19
  this.symbols = new Map();
19
20
  this.children = [];
21
+ this.startLoc = null; // { line, column } for positional scope lookup
22
+ this.endLoc = null;
20
23
  }
21
24
 
22
25
  define(name, symbol) {
23
26
  if (this.symbols.has(name)) {
27
+ const existing = this.symbols.get(name);
28
+ // Allow user code to shadow builtins
29
+ if (existing.kind === 'builtin') {
30
+ this.symbols.set(name, symbol);
31
+ return;
32
+ }
24
33
  throw new Error(
25
34
  `${symbol.loc.file}:${symbol.loc.line}:${symbol.loc.column} — '${name}' is already defined in this scope`
26
35
  );
@@ -57,4 +66,32 @@ export class Scope {
57
66
  this.children.push(c);
58
67
  return c;
59
68
  }
69
+
70
+ /**
71
+ * Find the narrowest scope containing a given position.
72
+ */
73
+ findScopeAtPosition(line, column) {
74
+ // Check children first (narrower scopes)
75
+ for (const child of this.children) {
76
+ if (child.startLoc && child.endLoc) {
77
+ if ((line > child.startLoc.line || (line === child.startLoc.line && column >= child.startLoc.column)) &&
78
+ (line < child.endLoc.line || (line === child.endLoc.line && column <= child.endLoc.column))) {
79
+ const nested = child.findScopeAtPosition(line, column);
80
+ return nested || child;
81
+ }
82
+ } else {
83
+ // No position info — recurse anyway
84
+ const nested = child.findScopeAtPosition(line, column);
85
+ if (nested) return nested;
86
+ }
87
+ }
88
+ // If this scope contains the position, return this
89
+ if (this.startLoc && this.endLoc) {
90
+ if ((line > this.startLoc.line || (line === this.startLoc.line && column >= this.startLoc.column)) &&
91
+ (line < this.endLoc.line || (line === this.endLoc.line && column <= this.endLoc.column))) {
92
+ return this;
93
+ }
94
+ }
95
+ return null;
96
+ }
60
97
  }