tova 0.3.5 → 0.3.6

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.3.5",
3
+ "version": "0.3.6",
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",
@@ -12,6 +12,10 @@ import {
12
12
  } from './types.js';
13
13
  import { ErrorCode, WarningCode } from '../diagnostics/error-codes.js';
14
14
 
15
+ // Pre-allocated constants for hot-path type checking (avoid per-call allocation)
16
+ const ARITHMETIC_OPS = new Set(['-', '*', '/', '%', '**']);
17
+ const NUMERIC_TYPES = new Set(['Int', 'Float']);
18
+
15
19
  const _JS_GLOBALS = new Set([
16
20
  'console', 'document', 'window', 'globalThis', 'self',
17
21
  'JSON', 'Math', 'Date', 'RegExp', 'Error', 'TypeError', 'RangeError',
@@ -35,17 +39,22 @@ const _JS_GLOBALS = new Set([
35
39
  function levenshtein(a, b) {
36
40
  if (a.length === 0) return b.length;
37
41
  if (b.length === 0) return a.length;
38
- const m = [];
39
- for (let i = 0; i <= b.length; i++) m[i] = [i];
40
- for (let j = 0; j <= a.length; j++) m[0][j] = j;
42
+ // Ensure a is the shorter string for O(min(n,m)) space
43
+ if (a.length > b.length) { const t = a; a = b; b = t; }
44
+ const len = a.length;
45
+ let prev = new Array(len + 1);
46
+ let curr = new Array(len + 1);
47
+ for (let j = 0; j <= len; j++) prev[j] = j;
41
48
  for (let i = 1; i <= b.length; i++) {
42
- for (let j = 1; j <= a.length; j++) {
43
- m[i][j] = b[i-1] === a[j-1]
44
- ? m[i-1][j-1]
45
- : Math.min(m[i-1][j-1] + 1, m[i][j-1] + 1, m[i-1][j] + 1);
49
+ curr[0] = i;
50
+ for (let j = 1; j <= len; j++) {
51
+ curr[j] = b[i-1] === a[j-1]
52
+ ? prev[j-1]
53
+ : Math.min(prev[j-1], curr[j-1], prev[j]) + 1;
46
54
  }
55
+ const tmp = prev; prev = curr; curr = tmp;
47
56
  }
48
- return m[b.length][a.length];
57
+ return prev[len];
49
58
  }
50
59
 
51
60
  const _TOVA_RUNTIME = new Set([
@@ -53,6 +62,19 @@ const _TOVA_RUNTIME = new Set([
53
62
  'db', 'server', 'client', 'shared',
54
63
  ]);
55
64
 
65
+ // Pre-built static candidate set for Levenshtein suggestions (N1 optimization)
66
+ // Lazily initialized on first use since BUILTIN_NAMES, _JS_GLOBALS, _TOVA_RUNTIME never change.
67
+ let _STATIC_SUGGESTION_NAMES = null;
68
+ function _getStaticSuggestionNames() {
69
+ if (!_STATIC_SUGGESTION_NAMES) {
70
+ _STATIC_SUGGESTION_NAMES = [];
71
+ for (const n of BUILTIN_NAMES) _STATIC_SUGGESTION_NAMES.push(n);
72
+ for (const n of _JS_GLOBALS) _STATIC_SUGGESTION_NAMES.push(n);
73
+ for (const n of _TOVA_RUNTIME) _STATIC_SUGGESTION_NAMES.push(n);
74
+ }
75
+ return _STATIC_SUGGESTION_NAMES;
76
+ }
77
+
56
78
  export class Analyzer {
57
79
  constructor(ast, filename = '<stdin>', options = {}) {
58
80
  this.ast = ast;
@@ -564,8 +586,17 @@ export class Analyzer {
564
586
 
565
587
  _parseGenericType(typeStr) {
566
588
  if (!typeStr) return { base: typeStr, params: [] };
589
+ // Check cache first
590
+ if (!this._parseGenericCache) this._parseGenericCache = new Map();
591
+ const cached = this._parseGenericCache.get(typeStr);
592
+ if (cached) return cached;
593
+
567
594
  const ltIdx = typeStr.indexOf('<');
568
- if (ltIdx === -1) return { base: typeStr, params: [] };
595
+ if (ltIdx === -1) {
596
+ const result = { base: typeStr, params: [] };
597
+ this._parseGenericCache.set(typeStr, result);
598
+ return result;
599
+ }
569
600
  const base = typeStr.slice(0, ltIdx);
570
601
  const inner = typeStr.slice(ltIdx + 1, typeStr.lastIndexOf('>'));
571
602
  // Split on top-level commas (respecting nested <>)
@@ -581,7 +612,9 @@ export class Analyzer {
581
612
  }
582
613
  }
583
614
  params.push(inner.slice(start).trim());
584
- return { base, params };
615
+ const result = { base, params };
616
+ this._parseGenericCache.set(typeStr, result);
617
+ return result;
585
618
  }
586
619
 
587
620
  _typesCompatible(expected, actual) {
@@ -950,6 +983,13 @@ export class Analyzer {
950
983
  // Check if any target is already defined (immutable reassignment check)
951
984
  for (let i = 0; i < node.targets.length; i++) {
952
985
  const target = node.targets[i];
986
+
987
+ // Complex targets (e.g., arr[i] = val, obj.prop = val) — visit and skip declaration logic
988
+ if (typeof target !== 'string') {
989
+ this.visitExpression(target);
990
+ continue;
991
+ }
992
+
953
993
  const existing = this._lookupAssignTarget(target);
954
994
  if (existing) {
955
995
  // Allow user code to shadow builtins (e.g., url = "/api")
@@ -1095,7 +1135,9 @@ export class Analyzer {
1095
1135
  this.visitNode(node.body);
1096
1136
 
1097
1137
  // Return path analysis: check that all paths return a value
1098
- if (expectedReturn && node.body.type === 'BlockStatement') {
1138
+ // Skip for @wasm/@fast functions they use implicit returns or specialized codegen
1139
+ const isWasm = node.decorators && node.decorators.some(d => d.name === 'wasm' || d.name === 'fast');
1140
+ if (expectedReturn && node.body.type === 'BlockStatement' && !isWasm) {
1099
1141
  if (!this._definitelyReturns(node.body)) {
1100
1142
  this.warn(`Function '${node.name}' declares return type ${expectedReturn} but not all code paths return a value`, node.loc, null, { code: 'W205' });
1101
1143
  }
@@ -1660,22 +1702,36 @@ export class Analyzer {
1660
1702
  }
1661
1703
 
1662
1704
  _findClosestMatch(name) {
1663
- const candidates = new Set();
1705
+ const candidates = [];
1706
+ // Collect scope symbols (these change per call)
1664
1707
  let scope = this.currentScope;
1665
1708
  while (scope) {
1666
- for (const n of scope.symbols.keys()) candidates.add(n);
1709
+ for (const n of scope.symbols.keys()) candidates.push(n);
1667
1710
  scope = scope.parent;
1668
1711
  }
1669
- for (const n of BUILTIN_NAMES) candidates.add(n);
1670
- for (const n of _JS_GLOBALS) candidates.add(n);
1671
- for (const n of _TOVA_RUNTIME) candidates.add(n);
1672
1712
 
1673
1713
  let best = null;
1674
1714
  let bestDist = Infinity;
1675
1715
  const maxDist = Math.max(2, Math.floor(name.length * 0.4));
1676
- for (const c of candidates) {
1716
+ const nameLower = name.toLowerCase();
1717
+
1718
+ // Check scope symbols first
1719
+ for (let i = 0; i < candidates.length; i++) {
1720
+ const c = candidates[i];
1677
1721
  if (Math.abs(c.length - name.length) > maxDist) continue;
1678
- const d = levenshtein(name.toLowerCase(), c.toLowerCase());
1722
+ const d = levenshtein(nameLower, c.toLowerCase());
1723
+ if (d < bestDist && d <= maxDist && d > 0) {
1724
+ bestDist = d;
1725
+ best = c;
1726
+ }
1727
+ }
1728
+
1729
+ // Check static global names (BUILTIN_NAMES, _JS_GLOBALS, _TOVA_RUNTIME)
1730
+ const staticNames = _getStaticSuggestionNames();
1731
+ for (let i = 0; i < staticNames.length; i++) {
1732
+ const c = staticNames[i];
1733
+ if (Math.abs(c.length - name.length) > maxDist) continue;
1734
+ const d = levenshtein(nameLower, c.toLowerCase());
1679
1735
  if (d < bestDist && d <= maxDist && d > 0) {
1680
1736
  bestDist = d;
1681
1737
  best = c;
@@ -1853,9 +1909,28 @@ export class Analyzer {
1853
1909
  // contain ALL covered variant names (avoids false positives with shared names)
1854
1910
  const candidates = [];
1855
1911
  this._collectTypeCandidates(this.ast.body, coveredVariants, candidates);
1856
- // Only warn if exactly one type contains all covered variants
1912
+
1913
+ let matched = null;
1857
1914
  if (candidates.length === 1) {
1858
- const [typeName, typeVariants] = candidates[0];
1915
+ matched = candidates[0];
1916
+ } else if (candidates.length > 1) {
1917
+ // Disambiguate using subject's inferred type name
1918
+ let subjectTypeName = null;
1919
+ if (node.subject) {
1920
+ subjectTypeName = this._inferType(node.subject);
1921
+ if (!subjectTypeName && node.subject.type === 'Identifier') {
1922
+ const sym = this.currentScope.lookup(node.subject.name);
1923
+ if (sym) subjectTypeName = sym.inferredType;
1924
+ }
1925
+ }
1926
+ if (subjectTypeName) {
1927
+ const exact = candidates.find(([name]) => name === subjectTypeName);
1928
+ if (exact) matched = exact;
1929
+ }
1930
+ }
1931
+
1932
+ if (matched) {
1933
+ const [typeName, typeVariants] = matched;
1859
1934
  for (const v of typeVariants) {
1860
1935
  if (!coveredVariants.has(v)) {
1861
1936
  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' });
@@ -1865,6 +1940,70 @@ export class Analyzer {
1865
1940
  }
1866
1941
  }
1867
1942
 
1943
+ // Check if a match expression covers all variants without a wildcard (for return path analysis)
1944
+ _isMatchExhaustive(node) {
1945
+ const coveredVariants = new Set();
1946
+ for (const arm of node.arms) {
1947
+ if (arm.pattern.type === 'VariantPattern') {
1948
+ coveredVariants.add(arm.pattern.name);
1949
+ }
1950
+ }
1951
+ if (coveredVariants.size === 0) return false;
1952
+
1953
+ // Check subject's inferred type
1954
+ if (node.subject) {
1955
+ const subjectTypeStr = this._inferType(node.subject);
1956
+ if (subjectTypeStr) {
1957
+ const typeStructure = this.typeRegistry.types.get(subjectTypeStr);
1958
+ if (typeStructure instanceof ADTType) {
1959
+ return typeStructure.getVariantNames().every(v => coveredVariants.has(v));
1960
+ }
1961
+ }
1962
+ if (node.subject.type === 'Identifier') {
1963
+ const sym = this.currentScope.lookup(node.subject.name);
1964
+ if (sym && sym.inferredType) {
1965
+ const typeStructure = this.typeRegistry.types.get(sym.inferredType);
1966
+ if (typeStructure instanceof ADTType) {
1967
+ return typeStructure.getVariantNames().every(v => coveredVariants.has(v));
1968
+ }
1969
+ }
1970
+ }
1971
+ }
1972
+
1973
+ // Built-in Result type
1974
+ if ((coveredVariants.has('Ok') || coveredVariants.has('Err')) &&
1975
+ coveredVariants.has('Ok') && coveredVariants.has('Err')) return true;
1976
+ // Built-in Option type
1977
+ if ((coveredVariants.has('Some') || coveredVariants.has('None')) &&
1978
+ coveredVariants.has('Some') && coveredVariants.has('None')) return true;
1979
+
1980
+ // Fallback: check user-defined types
1981
+ const candidates = [];
1982
+ this._collectTypeCandidates(this.ast.body, coveredVariants, candidates);
1983
+
1984
+ let matched = null;
1985
+ if (candidates.length === 1) {
1986
+ matched = candidates[0];
1987
+ } else if (candidates.length > 1 && node.subject) {
1988
+ // Disambiguate using subject's inferred type name
1989
+ let subjectTypeName = this._inferType(node.subject);
1990
+ if (!subjectTypeName && node.subject.type === 'Identifier') {
1991
+ const sym = this.currentScope.lookup(node.subject.name);
1992
+ if (sym) subjectTypeName = sym.inferredType;
1993
+ }
1994
+ if (subjectTypeName) {
1995
+ const exact = candidates.find(([name]) => name === subjectTypeName);
1996
+ if (exact) matched = exact;
1997
+ }
1998
+ }
1999
+
2000
+ if (matched) {
2001
+ const [, typeVariants] = matched;
2002
+ return typeVariants.every(v => coveredVariants.has(v));
2003
+ }
2004
+ return false;
2005
+ }
2006
+
1868
2007
  _collectTypeCandidates(nodes, coveredVariants, candidates) {
1869
2008
  for (const node of nodes) {
1870
2009
  if (node.type === 'TypeDeclaration') {
@@ -2015,8 +2154,12 @@ export class Analyzer {
2015
2154
  arm.pattern.type === 'WildcardPattern' ||
2016
2155
  (arm.pattern.type === 'BindingPattern' && !arm.guard)
2017
2156
  );
2018
- if (!hasWildcard) return false;
2019
- return node.arms.every(arm => this._definitelyReturns(arm.body));
2157
+ const isExhaustive = hasWildcard || this._isMatchExhaustive(node);
2158
+ if (!isExhaustive) return false;
2159
+ // Match arms with expression bodies (not block statements) are implicit returns
2160
+ return node.arms.every(arm =>
2161
+ arm.body.type !== 'BlockStatement' || this._definitelyReturns(arm.body)
2162
+ );
2020
2163
  }
2021
2164
  case 'TryCatchStatement': {
2022
2165
  const tryReturns = node.tryBody.length > 0 &&
@@ -2202,7 +2345,7 @@ export class Analyzer {
2202
2345
  if (rightType && rightType !== 'String' && rightType !== 'Any') {
2203
2346
  this.strictError(`Type mismatch: '++' expects String on right side, but got ${rightType}`, node.loc, "try toString(value) to convert");
2204
2347
  }
2205
- } else if (['-', '*', '/', '%', '**'].includes(op)) {
2348
+ } else if (ARITHMETIC_OPS.has(op)) {
2206
2349
  // String literal * Int is valid (string repeat) — skip warning for that case
2207
2350
  if (op === '*') {
2208
2351
  const leftIsStr = node.left.type === 'StringLiteral' || node.left.type === 'TemplateLiteral';
@@ -2210,23 +2353,21 @@ export class Analyzer {
2210
2353
  if (leftIsStr || rightIsStr) return;
2211
2354
  }
2212
2355
  // Arithmetic: both sides must be numeric
2213
- const numerics = new Set(['Int', 'Float']);
2214
- if (leftType && !numerics.has(leftType) && leftType !== 'Any') {
2356
+ if (leftType && !NUMERIC_TYPES.has(leftType) && leftType !== 'Any') {
2215
2357
  const hint = leftType === 'String' ? "try toInt(value) or toFloat(value) to parse" : null;
2216
2358
  this.strictError(`Type mismatch: '${op}' expects numeric type, but got ${leftType}`, node.loc, hint);
2217
2359
  }
2218
- if (rightType && !numerics.has(rightType) && rightType !== 'Any') {
2360
+ if (rightType && !NUMERIC_TYPES.has(rightType) && rightType !== 'Any') {
2219
2361
  const hint = rightType === 'String' ? "try toInt(value) or toFloat(value) to parse" : null;
2220
2362
  this.strictError(`Type mismatch: '${op}' expects numeric type, but got ${rightType}`, node.loc, hint);
2221
2363
  }
2222
2364
  } else if (op === '+') {
2223
2365
  // Addition: both sides must be numeric (Tova uses ++ for strings)
2224
- const numerics = new Set(['Int', 'Float']);
2225
- if (leftType && !numerics.has(leftType) && leftType !== 'Any') {
2366
+ if (leftType && !NUMERIC_TYPES.has(leftType) && leftType !== 'Any') {
2226
2367
  const hint = leftType === 'String' ? "try toInt(value) or toFloat(value) to parse" : null;
2227
2368
  this.strictError(`Type mismatch: '+' expects numeric type, but got ${leftType}`, node.loc, hint);
2228
2369
  }
2229
- if (rightType && !numerics.has(rightType) && rightType !== 'Any') {
2370
+ if (rightType && !NUMERIC_TYPES.has(rightType) && rightType !== 'Any') {
2230
2371
  const hint = rightType === 'String' ? "try toInt(value) or toFloat(value) to parse" : null;
2231
2372
  this.strictError(`Type mismatch: '+' expects numeric type, but got ${rightType}`, node.loc, hint);
2232
2373
  }
@@ -159,11 +159,27 @@ export function installClientAnalyzer(AnalyzerClass) {
159
159
  this.currentScope = this.currentScope.child('block');
160
160
  try {
161
161
  this.visitExpression(node.iterable);
162
- try {
163
- this.currentScope.define(node.variable,
164
- new Symbol(node.variable, 'variable', null, false, node.loc));
165
- } catch (e) {
166
- this.error(e.message);
162
+ const variable = node.variable;
163
+ if (typeof variable === 'string') {
164
+ try {
165
+ this.currentScope.define(variable,
166
+ new Symbol(variable, 'variable', null, false, node.loc));
167
+ } catch (e) { this.error(e.message); }
168
+ } else if (variable.type === 'ArrayPattern') {
169
+ for (const el of variable.elements) {
170
+ try {
171
+ this.currentScope.define(el,
172
+ new Symbol(el, 'variable', null, false, variable.loc));
173
+ } catch (e) { this.error(e.message); }
174
+ }
175
+ } else if (variable.type === 'ObjectPattern') {
176
+ for (const prop of variable.properties) {
177
+ const name = prop.value || prop.key;
178
+ try {
179
+ this.currentScope.define(name,
180
+ new Symbol(name, 'variable', null, false, variable.loc));
181
+ } catch (e) { this.error(e.message); }
182
+ }
167
183
  }
168
184
  for (const child of node.body) {
169
185
  this.visitNode(child);
@@ -20,6 +20,7 @@ export class Scope {
20
20
  this.children = [];
21
21
  this.startLoc = null; // { line, column } for positional scope lookup
22
22
  this.endLoc = null;
23
+ this._indexed = false;
23
24
  }
24
25
 
25
26
  define(name, symbol) {
@@ -67,11 +68,35 @@ export class Scope {
67
68
  return c;
68
69
  }
69
70
 
71
+ /**
72
+ * Build a sorted index of children for fast binary-search lookup.
73
+ * Call once after analysis is complete.
74
+ */
75
+ buildIndex() {
76
+ // Sort children with position info by start line, then column
77
+ if (this.children.length > 1) {
78
+ this.children.sort((a, b) => {
79
+ if (!a.startLoc) return 1;
80
+ if (!b.startLoc) return -1;
81
+ if (a.startLoc.line !== b.startLoc.line) return a.startLoc.line - b.startLoc.line;
82
+ return a.startLoc.column - b.startLoc.column;
83
+ });
84
+ }
85
+ this._indexed = true;
86
+ for (const child of this.children) {
87
+ child.buildIndex();
88
+ }
89
+ }
90
+
70
91
  /**
71
92
  * Find the narrowest scope containing a given position.
93
+ * Uses binary search if buildIndex() has been called.
72
94
  */
73
95
  findScopeAtPosition(line, column) {
74
- // Check children first (narrower scopes)
96
+ if (this._indexed && this.children.length > 4) {
97
+ return this._findScopeIndexed(line, column);
98
+ }
99
+ // Linear fallback for small lists or un-indexed scopes
75
100
  for (const child of this.children) {
76
101
  if (child.startLoc && child.endLoc) {
77
102
  if ((line > child.startLoc.line || (line === child.startLoc.line && column >= child.startLoc.column)) &&
@@ -80,12 +105,62 @@ export class Scope {
80
105
  return nested || child;
81
106
  }
82
107
  } else {
83
- // No position info — recurse anyway
84
108
  const nested = child.findScopeAtPosition(line, column);
85
109
  if (nested) return nested;
86
110
  }
87
111
  }
88
- // If this scope contains the position, return this
112
+ if (this.startLoc && this.endLoc) {
113
+ if ((line > this.startLoc.line || (line === this.startLoc.line && column >= this.startLoc.column)) &&
114
+ (line < this.endLoc.line || (line === this.endLoc.line && column <= this.endLoc.column))) {
115
+ return this;
116
+ }
117
+ }
118
+ return null;
119
+ }
120
+
121
+ _findScopeIndexed(line, column) {
122
+ // Binary search for the last child whose start is <= target position
123
+ const children = this.children;
124
+ let lo = 0, hi = children.length - 1;
125
+ let candidate = -1;
126
+
127
+ while (lo <= hi) {
128
+ const mid = (lo + hi) >> 1;
129
+ const c = children[mid];
130
+ if (!c.startLoc) { lo = mid + 1; continue; }
131
+ if (c.startLoc.line < line || (c.startLoc.line === line && c.startLoc.column <= column)) {
132
+ candidate = mid;
133
+ lo = mid + 1;
134
+ } else {
135
+ hi = mid - 1;
136
+ }
137
+ }
138
+
139
+ // Check candidate and neighbors (scopes can nest, so check a small window)
140
+ if (candidate >= 0) {
141
+ // Check candidate and up to 2 before it (overlapping scopes)
142
+ const start = Math.max(0, candidate - 2);
143
+ const end = Math.min(children.length - 1, candidate + 1);
144
+ for (let i = start; i <= end; i++) {
145
+ const child = children[i];
146
+ if (child.startLoc && child.endLoc) {
147
+ if ((line > child.startLoc.line || (line === child.startLoc.line && column >= child.startLoc.column)) &&
148
+ (line < child.endLoc.line || (line === child.endLoc.line && column <= child.endLoc.column))) {
149
+ const nested = child.findScopeAtPosition(line, column);
150
+ return nested || child;
151
+ }
152
+ }
153
+ }
154
+ }
155
+
156
+ // Fallback: check children without position info
157
+ for (const child of children) {
158
+ if (!child.startLoc) {
159
+ const nested = child.findScopeAtPosition(line, column);
160
+ if (nested) return nested;
161
+ }
162
+ }
163
+
89
164
  if (this.startLoc && this.endLoc) {
90
165
  if ((line > this.startLoc.line || (line === this.startLoc.line && column >= this.startLoc.column)) &&
91
166
  (line < this.endLoc.line || (line === this.endLoc.line && column <= this.endLoc.column))) {