tova 0.3.4 → 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/bin/tova.js +438 -58
- package/package.json +1 -1
- package/src/analyzer/analyzer.js +172 -32
- package/src/analyzer/client-analyzer.js +21 -5
- package/src/analyzer/scope.js +78 -3
- package/src/codegen/base-codegen.js +754 -45
- package/src/codegen/client-codegen.js +293 -36
- package/src/codegen/codegen.js +10 -15
- package/src/codegen/server-codegen.js +189 -40
- package/src/codegen/wasm-codegen.js +610 -0
- package/src/lexer/lexer.js +157 -109
- package/src/lexer/tokens.js +3 -0
- package/src/lsp/server.js +148 -12
- package/src/parser/ast.js +2 -1
- package/src/parser/client-parser.js +10 -3
- package/src/parser/parser.js +144 -150
- package/src/runtime/embedded.js +1 -1
- package/src/runtime/reactivity.js +307 -59
- package/src/runtime/ssr.js +101 -34
- package/src/stdlib/inline.js +333 -24
- package/src/stdlib/native-bridge.js +150 -0
- package/src/version.js +1 -1
package/package.json
CHANGED
package/src/analyzer/analyzer.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { Scope, Symbol } from './scope.js';
|
|
2
2
|
import { PIPE_TARGET } from '../parser/ast.js';
|
|
3
3
|
import { BUILTIN_NAMES } from '../stdlib/inline.js';
|
|
4
|
+
import { collectServerBlockFunctions, installServerAnalyzer } from './server-analyzer.js';
|
|
5
|
+
import { installClientAnalyzer } from './client-analyzer.js';
|
|
4
6
|
import {
|
|
5
7
|
Type, PrimitiveType, NilType, AnyType, UnknownType,
|
|
6
8
|
ArrayType, TupleType, FunctionType, RecordType, ADTType,
|
|
@@ -10,6 +12,10 @@ import {
|
|
|
10
12
|
} from './types.js';
|
|
11
13
|
import { ErrorCode, WarningCode } from '../diagnostics/error-codes.js';
|
|
12
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
|
+
|
|
13
19
|
const _JS_GLOBALS = new Set([
|
|
14
20
|
'console', 'document', 'window', 'globalThis', 'self',
|
|
15
21
|
'JSON', 'Math', 'Date', 'RegExp', 'Error', 'TypeError', 'RangeError',
|
|
@@ -33,17 +39,22 @@ const _JS_GLOBALS = new Set([
|
|
|
33
39
|
function levenshtein(a, b) {
|
|
34
40
|
if (a.length === 0) return b.length;
|
|
35
41
|
if (b.length === 0) return a.length;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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;
|
|
39
48
|
for (let i = 1; i <= b.length; i++) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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;
|
|
44
54
|
}
|
|
55
|
+
const tmp = prev; prev = curr; curr = tmp;
|
|
45
56
|
}
|
|
46
|
-
return
|
|
57
|
+
return prev[len];
|
|
47
58
|
}
|
|
48
59
|
|
|
49
60
|
const _TOVA_RUNTIME = new Set([
|
|
@@ -51,6 +62,19 @@ const _TOVA_RUNTIME = new Set([
|
|
|
51
62
|
'db', 'server', 'client', 'shared',
|
|
52
63
|
]);
|
|
53
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
|
+
|
|
54
78
|
export class Analyzer {
|
|
55
79
|
constructor(ast, filename = '<stdin>', options = {}) {
|
|
56
80
|
this.ast = ast;
|
|
@@ -267,7 +291,6 @@ export class Analyzer {
|
|
|
267
291
|
// Pre-pass: collect named server block functions for inter-server RPC validation
|
|
268
292
|
const hasServerBlocks = this.ast.body.some(n => n.type === 'ServerBlock');
|
|
269
293
|
if (hasServerBlocks) {
|
|
270
|
-
const { collectServerBlockFunctions, installServerAnalyzer } = import.meta.require('./server-analyzer.js');
|
|
271
294
|
installServerAnalyzer(Analyzer);
|
|
272
295
|
this.serverBlockFunctions = collectServerBlockFunctions(this.ast);
|
|
273
296
|
} else {
|
|
@@ -563,8 +586,17 @@ export class Analyzer {
|
|
|
563
586
|
|
|
564
587
|
_parseGenericType(typeStr) {
|
|
565
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
|
+
|
|
566
594
|
const ltIdx = typeStr.indexOf('<');
|
|
567
|
-
if (ltIdx === -1)
|
|
595
|
+
if (ltIdx === -1) {
|
|
596
|
+
const result = { base: typeStr, params: [] };
|
|
597
|
+
this._parseGenericCache.set(typeStr, result);
|
|
598
|
+
return result;
|
|
599
|
+
}
|
|
568
600
|
const base = typeStr.slice(0, ltIdx);
|
|
569
601
|
const inner = typeStr.slice(ltIdx + 1, typeStr.lastIndexOf('>'));
|
|
570
602
|
// Split on top-level commas (respecting nested <>)
|
|
@@ -580,7 +612,9 @@ export class Analyzer {
|
|
|
580
612
|
}
|
|
581
613
|
}
|
|
582
614
|
params.push(inner.slice(start).trim());
|
|
583
|
-
|
|
615
|
+
const result = { base, params };
|
|
616
|
+
this._parseGenericCache.set(typeStr, result);
|
|
617
|
+
return result;
|
|
584
618
|
}
|
|
585
619
|
|
|
586
620
|
_typesCompatible(expected, actual) {
|
|
@@ -734,7 +768,6 @@ export class Analyzer {
|
|
|
734
768
|
|
|
735
769
|
_visitServerNode(node) {
|
|
736
770
|
if (!Analyzer.prototype._serverAnalyzerInstalled) {
|
|
737
|
-
const { installServerAnalyzer } = import.meta.require('./server-analyzer.js');
|
|
738
771
|
installServerAnalyzer(Analyzer);
|
|
739
772
|
}
|
|
740
773
|
const methodName = 'visit' + node.type;
|
|
@@ -743,7 +776,6 @@ export class Analyzer {
|
|
|
743
776
|
|
|
744
777
|
_visitClientNode(node) {
|
|
745
778
|
if (!Analyzer.prototype._clientAnalyzerInstalled) {
|
|
746
|
-
const { installClientAnalyzer } = import.meta.require('./client-analyzer.js');
|
|
747
779
|
installClientAnalyzer(Analyzer);
|
|
748
780
|
}
|
|
749
781
|
const methodName = 'visit' + node.type;
|
|
@@ -951,6 +983,13 @@ export class Analyzer {
|
|
|
951
983
|
// Check if any target is already defined (immutable reassignment check)
|
|
952
984
|
for (let i = 0; i < node.targets.length; i++) {
|
|
953
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
|
+
|
|
954
993
|
const existing = this._lookupAssignTarget(target);
|
|
955
994
|
if (existing) {
|
|
956
995
|
// Allow user code to shadow builtins (e.g., url = "/api")
|
|
@@ -1096,7 +1135,9 @@ export class Analyzer {
|
|
|
1096
1135
|
this.visitNode(node.body);
|
|
1097
1136
|
|
|
1098
1137
|
// Return path analysis: check that all paths return a value
|
|
1099
|
-
|
|
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) {
|
|
1100
1141
|
if (!this._definitelyReturns(node.body)) {
|
|
1101
1142
|
this.warn(`Function '${node.name}' declares return type ${expectedReturn} but not all code paths return a value`, node.loc, null, { code: 'W205' });
|
|
1102
1143
|
}
|
|
@@ -1661,22 +1702,36 @@ export class Analyzer {
|
|
|
1661
1702
|
}
|
|
1662
1703
|
|
|
1663
1704
|
_findClosestMatch(name) {
|
|
1664
|
-
const candidates =
|
|
1705
|
+
const candidates = [];
|
|
1706
|
+
// Collect scope symbols (these change per call)
|
|
1665
1707
|
let scope = this.currentScope;
|
|
1666
1708
|
while (scope) {
|
|
1667
|
-
for (const n of scope.symbols.keys()) candidates.
|
|
1709
|
+
for (const n of scope.symbols.keys()) candidates.push(n);
|
|
1668
1710
|
scope = scope.parent;
|
|
1669
1711
|
}
|
|
1670
|
-
for (const n of BUILTIN_NAMES) candidates.add(n);
|
|
1671
|
-
for (const n of _JS_GLOBALS) candidates.add(n);
|
|
1672
|
-
for (const n of _TOVA_RUNTIME) candidates.add(n);
|
|
1673
1712
|
|
|
1674
1713
|
let best = null;
|
|
1675
1714
|
let bestDist = Infinity;
|
|
1676
1715
|
const maxDist = Math.max(2, Math.floor(name.length * 0.4));
|
|
1677
|
-
|
|
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];
|
|
1678
1721
|
if (Math.abs(c.length - name.length) > maxDist) continue;
|
|
1679
|
-
const d = levenshtein(
|
|
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());
|
|
1680
1735
|
if (d < bestDist && d <= maxDist && d > 0) {
|
|
1681
1736
|
bestDist = d;
|
|
1682
1737
|
best = c;
|
|
@@ -1854,9 +1909,28 @@ export class Analyzer {
|
|
|
1854
1909
|
// contain ALL covered variant names (avoids false positives with shared names)
|
|
1855
1910
|
const candidates = [];
|
|
1856
1911
|
this._collectTypeCandidates(this.ast.body, coveredVariants, candidates);
|
|
1857
|
-
|
|
1912
|
+
|
|
1913
|
+
let matched = null;
|
|
1858
1914
|
if (candidates.length === 1) {
|
|
1859
|
-
|
|
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;
|
|
1860
1934
|
for (const v of typeVariants) {
|
|
1861
1935
|
if (!coveredVariants.has(v)) {
|
|
1862
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' });
|
|
@@ -1866,6 +1940,70 @@ export class Analyzer {
|
|
|
1866
1940
|
}
|
|
1867
1941
|
}
|
|
1868
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
|
+
|
|
1869
2007
|
_collectTypeCandidates(nodes, coveredVariants, candidates) {
|
|
1870
2008
|
for (const node of nodes) {
|
|
1871
2009
|
if (node.type === 'TypeDeclaration') {
|
|
@@ -2016,8 +2154,12 @@ export class Analyzer {
|
|
|
2016
2154
|
arm.pattern.type === 'WildcardPattern' ||
|
|
2017
2155
|
(arm.pattern.type === 'BindingPattern' && !arm.guard)
|
|
2018
2156
|
);
|
|
2019
|
-
|
|
2020
|
-
|
|
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
|
+
);
|
|
2021
2163
|
}
|
|
2022
2164
|
case 'TryCatchStatement': {
|
|
2023
2165
|
const tryReturns = node.tryBody.length > 0 &&
|
|
@@ -2203,7 +2345,7 @@ export class Analyzer {
|
|
|
2203
2345
|
if (rightType && rightType !== 'String' && rightType !== 'Any') {
|
|
2204
2346
|
this.strictError(`Type mismatch: '++' expects String on right side, but got ${rightType}`, node.loc, "try toString(value) to convert");
|
|
2205
2347
|
}
|
|
2206
|
-
} else if (
|
|
2348
|
+
} else if (ARITHMETIC_OPS.has(op)) {
|
|
2207
2349
|
// String literal * Int is valid (string repeat) — skip warning for that case
|
|
2208
2350
|
if (op === '*') {
|
|
2209
2351
|
const leftIsStr = node.left.type === 'StringLiteral' || node.left.type === 'TemplateLiteral';
|
|
@@ -2211,23 +2353,21 @@ export class Analyzer {
|
|
|
2211
2353
|
if (leftIsStr || rightIsStr) return;
|
|
2212
2354
|
}
|
|
2213
2355
|
// Arithmetic: both sides must be numeric
|
|
2214
|
-
|
|
2215
|
-
if (leftType && !numerics.has(leftType) && leftType !== 'Any') {
|
|
2356
|
+
if (leftType && !NUMERIC_TYPES.has(leftType) && leftType !== 'Any') {
|
|
2216
2357
|
const hint = leftType === 'String' ? "try toInt(value) or toFloat(value) to parse" : null;
|
|
2217
2358
|
this.strictError(`Type mismatch: '${op}' expects numeric type, but got ${leftType}`, node.loc, hint);
|
|
2218
2359
|
}
|
|
2219
|
-
if (rightType && !
|
|
2360
|
+
if (rightType && !NUMERIC_TYPES.has(rightType) && rightType !== 'Any') {
|
|
2220
2361
|
const hint = rightType === 'String' ? "try toInt(value) or toFloat(value) to parse" : null;
|
|
2221
2362
|
this.strictError(`Type mismatch: '${op}' expects numeric type, but got ${rightType}`, node.loc, hint);
|
|
2222
2363
|
}
|
|
2223
2364
|
} else if (op === '+') {
|
|
2224
2365
|
// Addition: both sides must be numeric (Tova uses ++ for strings)
|
|
2225
|
-
|
|
2226
|
-
if (leftType && !numerics.has(leftType) && leftType !== 'Any') {
|
|
2366
|
+
if (leftType && !NUMERIC_TYPES.has(leftType) && leftType !== 'Any') {
|
|
2227
2367
|
const hint = leftType === 'String' ? "try toInt(value) or toFloat(value) to parse" : null;
|
|
2228
2368
|
this.strictError(`Type mismatch: '+' expects numeric type, but got ${leftType}`, node.loc, hint);
|
|
2229
2369
|
}
|
|
2230
|
-
if (rightType && !
|
|
2370
|
+
if (rightType && !NUMERIC_TYPES.has(rightType) && rightType !== 'Any') {
|
|
2231
2371
|
const hint = rightType === 'String' ? "try toInt(value) or toFloat(value) to parse" : null;
|
|
2232
2372
|
this.strictError(`Type mismatch: '+' expects numeric type, but got ${rightType}`, node.loc, hint);
|
|
2233
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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);
|
package/src/analyzer/scope.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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))) {
|