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/bin/tova.js +438 -58
- package/package.json +1 -1
- package/src/analyzer/analyzer.js +170 -29
- 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 +6 -1
- 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 +142 -148
- 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
|
@@ -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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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.
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
1912
|
+
|
|
1913
|
+
let matched = null;
|
|
1857
1914
|
if (candidates.length === 1) {
|
|
1858
|
-
|
|
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
|
-
|
|
2019
|
-
|
|
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 (
|
|
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
|
-
|
|
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 && !
|
|
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
|
-
|
|
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 && !
|
|
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
|
-
|
|
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))) {
|