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/LICENSE +1 -1
- package/README.md +2 -0
- package/bin/tova.js +811 -154
- package/package.json +8 -2
- package/src/analyzer/analyzer.js +297 -58
- package/src/analyzer/scope.js +38 -1
- package/src/analyzer/type-registry.js +72 -0
- package/src/analyzer/types.js +478 -0
- package/src/codegen/base-codegen.js +371 -0
- package/src/codegen/client-codegen.js +62 -10
- package/src/codegen/codegen.js +111 -2
- package/src/codegen/server-codegen.js +175 -3
- package/src/config/edit-toml.js +100 -0
- package/src/config/package-json.js +52 -0
- package/src/config/resolve.js +100 -0
- package/src/config/toml.js +209 -0
- package/src/lexer/lexer.js +2 -2
- package/src/lsp/server.js +284 -30
- package/src/parser/ast.js +105 -0
- package/src/parser/parser.js +202 -2
- package/src/runtime/ai.js +305 -0
- package/src/runtime/devtools.js +228 -0
- package/src/runtime/embedded.js +3 -1
- package/src/runtime/io.js +240 -0
- package/src/runtime/reactivity.js +264 -19
- package/src/runtime/ssr.js +196 -24
- package/src/runtime/table.js +522 -0
- package/src/stdlib/collections.js +245 -0
- package/src/stdlib/core.js +87 -0
- package/src/stdlib/datetime.js +88 -0
- package/src/stdlib/encoding.js +35 -0
- package/src/stdlib/functional.js +82 -0
- package/src/stdlib/inline.js +334 -67
- package/src/stdlib/math.js +93 -0
- package/src/stdlib/string.js +95 -0
- package/src/stdlib/url.js +33 -0
- package/src/stdlib/validation.js +29 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tova",
|
|
3
|
-
"version": "0.
|
|
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
|
-
"
|
|
36
|
+
"author": "Enoch Kujem Abassey",
|
|
37
|
+
"keywords": [
|
|
38
|
+
"language",
|
|
39
|
+
"transpiler",
|
|
40
|
+
"fullstack",
|
|
41
|
+
"javascript"
|
|
42
|
+
],
|
|
37
43
|
"license": "MIT"
|
|
38
44
|
}
|
package/src/analyzer/analyzer.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
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
|
-
//
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
1961
|
-
|
|
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
|
-
|
|
1997
|
-
|
|
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
|
}
|
package/src/analyzer/scope.js
CHANGED
|
@@ -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
|
}
|