tova 0.9.5 → 0.9.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 +11 -0
- package/package.json +1 -1
- package/src/analyzer/analyzer.js +90 -2
- package/src/codegen/base-codegen.js +25 -1
- package/src/lexer/lexer.js +32 -3
- package/src/lexer/tokens.js +11 -0
- package/src/lsp/server.js +1 -1
- package/src/parser/ast.js +20 -0
- package/src/parser/parser.js +258 -19
- package/src/version.js +1 -1
package/bin/tova.js
CHANGED
|
@@ -5520,6 +5520,17 @@ function collectExports(ast, filename) {
|
|
|
5520
5520
|
if (node.isPublic) publicExports.add(node.name);
|
|
5521
5521
|
}
|
|
5522
5522
|
if (node.type === 'ImplDeclaration') { /* impl doesn't export a name */ }
|
|
5523
|
+
if (node.type === 'ReExportDeclaration') {
|
|
5524
|
+
if (node.specifiers) {
|
|
5525
|
+
// Named re-exports: pub { a, b as c } from "module"
|
|
5526
|
+
for (const spec of node.specifiers) {
|
|
5527
|
+
publicExports.add(spec.exported);
|
|
5528
|
+
allNames.add(spec.exported);
|
|
5529
|
+
}
|
|
5530
|
+
}
|
|
5531
|
+
// Wildcard re-exports: pub * from "module" — can't enumerate statically,
|
|
5532
|
+
// but mark as having re-exports so import validation can allow through
|
|
5533
|
+
}
|
|
5523
5534
|
}
|
|
5524
5535
|
|
|
5525
5536
|
for (const node of ast.body) {
|
package/package.json
CHANGED
package/src/analyzer/analyzer.js
CHANGED
|
@@ -13,6 +13,7 @@ import { ErrorCode, WarningCode } from '../diagnostics/error-codes.js';
|
|
|
13
13
|
|
|
14
14
|
// Pre-allocated constants for hot-path type checking (avoid per-call allocation)
|
|
15
15
|
const ARITHMETIC_OPS = new Set(['-', '*', '/', '%', '**']);
|
|
16
|
+
const BITWISE_OPS = new Set(['&', '|', '^', '<<', '>>', '>>>']);
|
|
16
17
|
const NUMERIC_TYPES = new Set(['Int', 'Float']);
|
|
17
18
|
|
|
18
19
|
const _JS_GLOBALS = new Set([
|
|
@@ -531,11 +532,13 @@ export class Analyzer {
|
|
|
531
532
|
if (lt === 'String' || rt === 'String') return 'String';
|
|
532
533
|
return 'Int';
|
|
533
534
|
}
|
|
535
|
+
if (BITWISE_OPS.has(expr.operator)) return 'Int';
|
|
534
536
|
if (['==', '!=', '<', '>', '<=', '>='].includes(expr.operator)) return 'Bool';
|
|
535
537
|
return null;
|
|
536
538
|
case 'UnaryExpression':
|
|
537
539
|
if (expr.operator === 'not' || expr.operator === '!') return 'Bool';
|
|
538
540
|
if (expr.operator === '-') return this._inferType(expr.operand);
|
|
541
|
+
if (expr.operator === '~') return 'Int';
|
|
539
542
|
return null;
|
|
540
543
|
case 'LogicalExpression':
|
|
541
544
|
return 'Bool';
|
|
@@ -772,6 +775,7 @@ export class Analyzer {
|
|
|
772
775
|
case 'ImportDeclaration': return this.visitImportDeclaration(node);
|
|
773
776
|
case 'ImportDefault': return this.visitImportDefault(node);
|
|
774
777
|
case 'ImportWildcard': return this.visitImportWildcard(node);
|
|
778
|
+
case 'ReExportDeclaration': return; // re-exports don't define local symbols
|
|
775
779
|
case 'IfStatement': return this.visitIfStatement(node);
|
|
776
780
|
case 'ForStatement': return this.visitForStatement(node);
|
|
777
781
|
case 'WhileStatement': return this.visitWhileStatement(node);
|
|
@@ -881,6 +885,22 @@ export class Analyzer {
|
|
|
881
885
|
// Argument count and type validation for known functions
|
|
882
886
|
this._checkCallArgCount(node);
|
|
883
887
|
this._checkCallArgTypes(node);
|
|
888
|
+
// W_UNSAFE_UNWRAP: detect .unwrap() and .expect() on Result/Option
|
|
889
|
+
if (node.callee.type === 'MemberExpression' && !node.callee.computed) {
|
|
890
|
+
const methodName = node.callee.property;
|
|
891
|
+
if (methodName === 'unwrap' || methodName === 'expect') {
|
|
892
|
+
const objType = this._inferType(node.callee.object);
|
|
893
|
+
if (objType && (objType.startsWith('Result') || objType.startsWith('Option') ||
|
|
894
|
+
objType === 'Result' || objType === 'Option')) {
|
|
895
|
+
this.warn(
|
|
896
|
+
`Unsafe ${methodName}() call on ${objType} — will panic if value is ${objType.startsWith('Result') ? 'Err' : 'None'}`,
|
|
897
|
+
node.loc,
|
|
898
|
+
'consider using unwrapOr(default) or match for safe error handling',
|
|
899
|
+
{ code: 'W_UNSAFE_UNWRAP' }
|
|
900
|
+
);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
}
|
|
884
904
|
// W_UNSAFE_INTERPOLATION: detect template literals in database calls
|
|
885
905
|
if (node.callee.type === 'MemberExpression' && !node.callee.computed) {
|
|
886
906
|
const prop = node.callee.property;
|
|
@@ -910,6 +930,49 @@ export class Analyzer {
|
|
|
910
930
|
case 'OptionalChain':
|
|
911
931
|
this.visitExpression(node.object);
|
|
912
932
|
if (node.computed) this.visitExpression(node.property);
|
|
933
|
+
// Type-aware checks (only for non-computed property access)
|
|
934
|
+
if (!node.computed && typeof node.property === 'string') {
|
|
935
|
+
const objType = this._inferType(node.object);
|
|
936
|
+
if (objType) {
|
|
937
|
+
// W_NULL_ACCESS: property access on nil
|
|
938
|
+
if (objType === 'Nil' && node.type !== 'OptionalChain') {
|
|
939
|
+
this.warn(
|
|
940
|
+
`Accessing property '${node.property}' on nil value`,
|
|
941
|
+
node.loc,
|
|
942
|
+
'this will always fail at runtime',
|
|
943
|
+
{ code: 'W_NULL_ACCESS' }
|
|
944
|
+
);
|
|
945
|
+
}
|
|
946
|
+
// W_NULL_ACCESS: property access on Option without optional chaining
|
|
947
|
+
else if ((objType === 'Option' || objType.startsWith('Option<')) && node.type !== 'OptionalChain') {
|
|
948
|
+
this.warn(
|
|
949
|
+
`Option value may be None — use '?.' or unwrap before accessing '${node.property}'`,
|
|
950
|
+
node.loc,
|
|
951
|
+
'consider using ?. for safe access or unwrapOr(default)',
|
|
952
|
+
{ code: 'W_NULL_ACCESS' }
|
|
953
|
+
);
|
|
954
|
+
}
|
|
955
|
+
// W_UNKNOWN_FIELD: access unknown field on user-defined type
|
|
956
|
+
else {
|
|
957
|
+
const BUILTIN_TYPES = new Set(['String', 'Int', 'Float', 'Bool', 'Array', 'Result', 'Option', 'Map', 'Set', 'Nil', 'Any']);
|
|
958
|
+
const baseType = objType.includes('<') ? objType.slice(0, objType.indexOf('<')) : objType;
|
|
959
|
+
if (!BUILTIN_TYPES.has(baseType) && !objType.startsWith('[') && !objType.startsWith('(')) {
|
|
960
|
+
const typeStructure = this.typeRegistry.types.get(objType);
|
|
961
|
+
if (typeStructure instanceof ADTType) {
|
|
962
|
+
const fieldType = typeStructure.getFieldType(node.property);
|
|
963
|
+
if (!fieldType) {
|
|
964
|
+
this.warn(
|
|
965
|
+
`Type '${objType}' has no field '${node.property}'`,
|
|
966
|
+
node.loc,
|
|
967
|
+
`available fields: ${[...new Set([...typeStructure.variants.values()].flatMap(v => [...v.keys()]))].join(', ') || 'none'}`,
|
|
968
|
+
{ code: 'W_UNKNOWN_FIELD' }
|
|
969
|
+
);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
}
|
|
913
976
|
return;
|
|
914
977
|
case 'PipeExpression':
|
|
915
978
|
this.visitExpression(node.left);
|
|
@@ -921,6 +984,23 @@ export class Analyzer {
|
|
|
921
984
|
return this.visitMatchExpression(node);
|
|
922
985
|
case 'ArrayLiteral':
|
|
923
986
|
for (const el of node.elements) this.visitExpression(el);
|
|
987
|
+
if (node.elements.length > 1) {
|
|
988
|
+
const firstType = this._inferType(node.elements[0]);
|
|
989
|
+
if (firstType && firstType !== 'Any') {
|
|
990
|
+
for (let i = 1; i < node.elements.length; i++) {
|
|
991
|
+
const elType = this._inferType(node.elements[i]);
|
|
992
|
+
if (elType && elType !== 'Any' && !this._typesCompatible(firstType, elType)) {
|
|
993
|
+
this.warn(
|
|
994
|
+
`Array contains mixed types: expected [${firstType}] but found ${elType} at index ${i}`,
|
|
995
|
+
node.elements[i].loc || node.loc,
|
|
996
|
+
`ensure all elements are ${firstType}`,
|
|
997
|
+
{ code: 'W209' }
|
|
998
|
+
);
|
|
999
|
+
break;
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
924
1004
|
return;
|
|
925
1005
|
case 'ObjectLiteral':
|
|
926
1006
|
for (const prop of node.properties) {
|
|
@@ -2111,8 +2191,8 @@ export class Analyzer {
|
|
|
2111
2191
|
if (!this._typesCompatible(existing.inferredType, newType)) {
|
|
2112
2192
|
this.strictError(`Type mismatch: '${target}' is ${existing.inferredType}, but assigned ${newType}`, node.loc, this._conversionHint(existing.inferredType, newType), { code: 'E102' });
|
|
2113
2193
|
}
|
|
2114
|
-
// Float narrowing warning
|
|
2115
|
-
if (
|
|
2194
|
+
// Float narrowing warning
|
|
2195
|
+
if (newType === 'Float' && existing.inferredType === 'Int') {
|
|
2116
2196
|
this.warn(`Potential data loss: assigning Float to Int variable '${target}'`, node.loc, "use floor() or round() for explicit conversion", { code: 'W204' });
|
|
2117
2197
|
}
|
|
2118
2198
|
}
|
|
@@ -3566,6 +3646,14 @@ export class Analyzer {
|
|
|
3566
3646
|
const hint = rightType === 'String' ? "try toInt(value) or toFloat(value) to parse" : null;
|
|
3567
3647
|
this.strictError(`Type mismatch: '+' expects numeric type, but got ${rightType}`, node.loc, hint);
|
|
3568
3648
|
}
|
|
3649
|
+
} else if (BITWISE_OPS.has(op)) {
|
|
3650
|
+
// Bitwise: both sides must be Int
|
|
3651
|
+
if (leftType && leftType !== 'Int' && leftType !== 'Any') {
|
|
3652
|
+
this.strictError(`Type mismatch: '${op}' expects Int, but got ${leftType}`, node.loc);
|
|
3653
|
+
}
|
|
3654
|
+
if (rightType && rightType !== 'Int' && rightType !== 'Any') {
|
|
3655
|
+
this.strictError(`Type mismatch: '${op}' expects Int, but got ${rightType}`, node.loc);
|
|
3656
|
+
}
|
|
3569
3657
|
}
|
|
3570
3658
|
}
|
|
3571
3659
|
|
|
@@ -279,6 +279,7 @@ export class BaseCodegen {
|
|
|
279
279
|
case 'ImportDeclaration': result = this.genImport(node); break;
|
|
280
280
|
case 'ImportDefault': result = this.genImportDefault(node); break;
|
|
281
281
|
case 'ImportWildcard': result = this.genImportWildcard(node); break;
|
|
282
|
+
case 'ReExportDeclaration': result = this.genReExport(node); break;
|
|
282
283
|
case 'IfStatement': result = this.genIfStatement(node); break;
|
|
283
284
|
case 'ForStatement': result = this.genForStatement(node); break;
|
|
284
285
|
case 'WhileStatement': result = this.genWhileStatement(node); break;
|
|
@@ -665,6 +666,19 @@ export class BaseCodegen {
|
|
|
665
666
|
return `${this.i()}import * as ${node.local} from ${JSON.stringify(node.source)};`;
|
|
666
667
|
}
|
|
667
668
|
|
|
669
|
+
genReExport(node) {
|
|
670
|
+
this._rewriteImportSource(node);
|
|
671
|
+
if (!node.specifiers) {
|
|
672
|
+
// pub * from "module" → export * from "module"
|
|
673
|
+
return `${this.i()}export * from ${JSON.stringify(node.source)};`;
|
|
674
|
+
}
|
|
675
|
+
// pub { a, b as c } from "module" → export { a, b as c } from "module"
|
|
676
|
+
const specs = node.specifiers.map(s =>
|
|
677
|
+
s.imported === s.exported ? s.imported : `${s.imported} as ${s.exported}`
|
|
678
|
+
).join(', ');
|
|
679
|
+
return `${this.i()}export { ${specs} } from ${JSON.stringify(node.source)};`;
|
|
680
|
+
}
|
|
681
|
+
|
|
668
682
|
genIfStatement(node) {
|
|
669
683
|
const p = [];
|
|
670
684
|
p.push(`${this.i()}if (${this.genExpression(node.condition)}) {\n`);
|
|
@@ -1301,7 +1315,7 @@ export class BaseCodegen {
|
|
|
1301
1315
|
genBinaryExpression(node) {
|
|
1302
1316
|
const op = node.operator;
|
|
1303
1317
|
|
|
1304
|
-
// Constant folding: arithmetic on two number literals
|
|
1318
|
+
// Constant folding: arithmetic and bitwise on two number literals
|
|
1305
1319
|
if (node.left.type === 'NumberLiteral' && node.right.type === 'NumberLiteral') {
|
|
1306
1320
|
const l = node.left.value, r = node.right.value;
|
|
1307
1321
|
let folded = null;
|
|
@@ -1312,6 +1326,12 @@ export class BaseCodegen {
|
|
|
1312
1326
|
case '/': if (r !== 0) folded = l / r; break;
|
|
1313
1327
|
case '%': if (r !== 0) folded = l % r; break;
|
|
1314
1328
|
case '**': folded = l ** r; break;
|
|
1329
|
+
case '&': folded = l & r; break;
|
|
1330
|
+
case '|': folded = l | r; break;
|
|
1331
|
+
case '^': folded = l ^ r; break;
|
|
1332
|
+
case '<<': folded = l << r; break;
|
|
1333
|
+
case '>>': folded = l >> r; break;
|
|
1334
|
+
case '>>>': folded = l >>> r; break;
|
|
1315
1335
|
}
|
|
1316
1336
|
if (folded !== null && Number.isFinite(folded)) {
|
|
1317
1337
|
return folded < 0 ? `(${folded})` : String(folded);
|
|
@@ -1348,6 +1368,10 @@ export class BaseCodegen {
|
|
|
1348
1368
|
}
|
|
1349
1369
|
|
|
1350
1370
|
genUnaryExpression(node) {
|
|
1371
|
+
// Constant folding: bitwise NOT on number literal
|
|
1372
|
+
if (node.operator === '~' && node.operand.type === 'NumberLiteral') {
|
|
1373
|
+
return String(~node.operand.value);
|
|
1374
|
+
}
|
|
1351
1375
|
const operand = this.genExpression(node.operand);
|
|
1352
1376
|
if (node.operator === 'not') return `(!${operand})`;
|
|
1353
1377
|
return `(${node.operator}${operand})`;
|
package/src/lexer/lexer.js
CHANGED
|
@@ -1089,6 +1089,20 @@ export class Lexer {
|
|
|
1089
1089
|
break;
|
|
1090
1090
|
|
|
1091
1091
|
case '<':
|
|
1092
|
+
if (this.peek() === '<') {
|
|
1093
|
+
// Left shift: << or <<=
|
|
1094
|
+
// Only treat as shift if previous token produces a value (avoids `< <JSXTag>`)
|
|
1095
|
+
const prevTok = this.tokens.length > 0 ? this.tokens[this.tokens.length - 1] : null;
|
|
1096
|
+
if (prevTok && Lexer.VALUE_TOKEN_TYPES.has(prevTok.type)) {
|
|
1097
|
+
this.advance(); // consume second <
|
|
1098
|
+
if (this.match('=')) {
|
|
1099
|
+
this.tokens.push(new Token(TokenType.LEFT_SHIFT_ASSIGN, '<<=', startLine, startCol));
|
|
1100
|
+
} else {
|
|
1101
|
+
this.tokens.push(new Token(TokenType.LEFT_SHIFT, '<<', startLine, startCol));
|
|
1102
|
+
}
|
|
1103
|
+
break;
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1092
1106
|
if (this.match('=')) {
|
|
1093
1107
|
this.tokens.push(new Token(TokenType.LESS_EQUAL, '<=', startLine, startCol));
|
|
1094
1108
|
} else {
|
|
@@ -1140,12 +1154,13 @@ export class Lexer {
|
|
|
1140
1154
|
case '&':
|
|
1141
1155
|
if (this.match('&')) {
|
|
1142
1156
|
this.tokens.push(new Token(TokenType.AND_AND, '&&', startLine, startCol));
|
|
1157
|
+
} else if (this.match('=')) {
|
|
1158
|
+
this.tokens.push(new Token(TokenType.BIT_AND_ASSIGN, '&=', startLine, startCol));
|
|
1143
1159
|
} else if (this._jsxStack.length > 0) {
|
|
1144
|
-
// Inside JSX, & is valid text
|
|
1145
|
-
// but handle gracefully by treating as text
|
|
1160
|
+
// Inside JSX, & is valid text
|
|
1146
1161
|
this.tokens.push(new Token(TokenType.JSX_TEXT, '&', startLine, startCol));
|
|
1147
1162
|
} else {
|
|
1148
|
-
this.
|
|
1163
|
+
this.tokens.push(new Token(TokenType.AMPERSAND, '&', startLine, startCol));
|
|
1149
1164
|
}
|
|
1150
1165
|
break;
|
|
1151
1166
|
|
|
@@ -1154,6 +1169,8 @@ export class Lexer {
|
|
|
1154
1169
|
this.tokens.push(new Token(TokenType.PIPE, '|>', startLine, startCol));
|
|
1155
1170
|
} else if (this.match('|')) {
|
|
1156
1171
|
this.tokens.push(new Token(TokenType.OR_OR, '||', startLine, startCol));
|
|
1172
|
+
} else if (this.match('=')) {
|
|
1173
|
+
this.tokens.push(new Token(TokenType.BIT_OR_ASSIGN, '|=', startLine, startCol));
|
|
1157
1174
|
} else {
|
|
1158
1175
|
this.tokens.push(new Token(TokenType.BAR, '|', startLine, startCol));
|
|
1159
1176
|
}
|
|
@@ -1195,6 +1212,18 @@ export class Lexer {
|
|
|
1195
1212
|
this.tokens.push(new Token(TokenType.AT, '@', startLine, startCol));
|
|
1196
1213
|
break;
|
|
1197
1214
|
|
|
1215
|
+
case '^':
|
|
1216
|
+
if (this.match('=')) {
|
|
1217
|
+
this.tokens.push(new Token(TokenType.BIT_XOR_ASSIGN, '^=', startLine, startCol));
|
|
1218
|
+
} else {
|
|
1219
|
+
this.tokens.push(new Token(TokenType.CARET, '^', startLine, startCol));
|
|
1220
|
+
}
|
|
1221
|
+
break;
|
|
1222
|
+
|
|
1223
|
+
case '~':
|
|
1224
|
+
this.tokens.push(new Token(TokenType.TILDE, '~', startLine, startCol));
|
|
1225
|
+
break;
|
|
1226
|
+
|
|
1198
1227
|
default:
|
|
1199
1228
|
this.error(`Unexpected character: '${ch}'`);
|
|
1200
1229
|
}
|
package/src/lexer/tokens.js
CHANGED
|
@@ -141,6 +141,17 @@ export const TokenType = {
|
|
|
141
141
|
MINUS_ASSIGN: 'MINUS_ASSIGN', // -=
|
|
142
142
|
STAR_ASSIGN: 'STAR_ASSIGN', // *=
|
|
143
143
|
SLASH_ASSIGN: 'SLASH_ASSIGN', // /=
|
|
144
|
+
AMPERSAND: 'AMPERSAND', // &
|
|
145
|
+
CARET: 'CARET', // ^
|
|
146
|
+
TILDE: 'TILDE', // ~
|
|
147
|
+
LEFT_SHIFT: 'LEFT_SHIFT', // <<
|
|
148
|
+
RIGHT_SHIFT: 'RIGHT_SHIFT', // >>
|
|
149
|
+
UNSIGNED_RIGHT_SHIFT: 'UNSIGNED_RIGHT_SHIFT', // >>>
|
|
150
|
+
BIT_AND_ASSIGN: 'BIT_AND_ASSIGN', // &=
|
|
151
|
+
BIT_OR_ASSIGN: 'BIT_OR_ASSIGN', // |=
|
|
152
|
+
BIT_XOR_ASSIGN: 'BIT_XOR_ASSIGN', // ^=
|
|
153
|
+
LEFT_SHIFT_ASSIGN: 'LEFT_SHIFT_ASSIGN', // <<=
|
|
154
|
+
RIGHT_SHIFT_ASSIGN: 'RIGHT_SHIFT_ASSIGN', // >>=
|
|
144
155
|
|
|
145
156
|
// Delimiters
|
|
146
157
|
LPAREN: 'LPAREN', // (
|
package/src/lsp/server.js
CHANGED
|
@@ -516,7 +516,7 @@ class TovaLanguageServer {
|
|
|
516
516
|
|
|
517
517
|
// Keywords
|
|
518
518
|
const keywords = [
|
|
519
|
-
'fn', '
|
|
519
|
+
'fn', 'if', 'elif', 'else', 'for', 'while', 'loop', 'when', 'in',
|
|
520
520
|
'return', 'match', 'type', 'import', 'from', 'true', 'false',
|
|
521
521
|
'nil', 'server', 'browser', 'client', 'shared', 'pub', 'mut',
|
|
522
522
|
'try', 'catch', 'finally', 'break', 'continue', 'async', 'await',
|
package/src/parser/ast.js
CHANGED
|
@@ -215,6 +215,26 @@ export class ImportWildcard {
|
|
|
215
215
|
}
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
+
// Re-export: pub { a, b as c } from "module"
|
|
219
|
+
export class ReExportDeclaration {
|
|
220
|
+
constructor(specifiers, source, loc) {
|
|
221
|
+
this.type = 'ReExportDeclaration';
|
|
222
|
+
this.specifiers = specifiers; // [{imported, exported}] or null for wildcard
|
|
223
|
+
this.source = source; // module path string
|
|
224
|
+
this.loc = loc;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Re-export specifier: individual name in pub { a, b as c } from "module"
|
|
229
|
+
export class ReExportSpecifier {
|
|
230
|
+
constructor(imported, exported, loc) {
|
|
231
|
+
this.type = 'ReExportSpecifier';
|
|
232
|
+
this.imported = imported; // original name
|
|
233
|
+
this.exported = exported; // re-exported name (same as imported if no alias)
|
|
234
|
+
this.loc = loc;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
218
238
|
// ============================================================
|
|
219
239
|
// Statements
|
|
220
240
|
// ============================================================
|
package/src/parser/parser.js
CHANGED
|
@@ -559,7 +559,7 @@ export class Parser {
|
|
|
559
559
|
if (this.check(TokenType.TYPE)) return this.parseTypeDeclaration();
|
|
560
560
|
if (this.check(TokenType.MUT)) this.error("'mut' is not supported in Tova. Use 'var' for mutable variables");
|
|
561
561
|
if (this.check(TokenType.VAR)) return this.parseVarDeclaration();
|
|
562
|
-
if (this.check(TokenType.LET))
|
|
562
|
+
if (this.check(TokenType.LET)) this.error("'let' is not needed in Tova. Destructure directly: {a, b} = obj, [a, b] = list, or (a, b) = pair");
|
|
563
563
|
if (this.check(TokenType.IF)) return this.parseIfStatement();
|
|
564
564
|
if (this.check(TokenType.FOR)) return this.parseForStatement();
|
|
565
565
|
if (this.check(TokenType.WHILE)) return this.parseWhileStatement();
|
|
@@ -599,6 +599,13 @@ export class Parser {
|
|
|
599
599
|
if (this.check(TokenType.PUB)) {
|
|
600
600
|
this.error("Duplicate 'pub' modifier");
|
|
601
601
|
}
|
|
602
|
+
// Re-export: pub { a, b } from "module" or pub * from "module"
|
|
603
|
+
if (this.check(TokenType.STAR) && this.peek(1).type === TokenType.FROM) {
|
|
604
|
+
return this.parseReExport(l);
|
|
605
|
+
}
|
|
606
|
+
if (this.check(TokenType.LBRACE) && this._looksLikeReExport()) {
|
|
607
|
+
return this.parseReExport(l);
|
|
608
|
+
}
|
|
602
609
|
// Handle pub component at top level (parseComponent is installed by browser-parser plugin)
|
|
603
610
|
if (this.check(TokenType.COMPONENT) && typeof this.parseComponent === 'function') {
|
|
604
611
|
const comp = this.parseComponent();
|
|
@@ -610,6 +617,61 @@ export class Parser {
|
|
|
610
617
|
return stmt;
|
|
611
618
|
}
|
|
612
619
|
|
|
620
|
+
// Check if pub { ... } is a re-export (identifiers with optional aliases, then } from "...")
|
|
621
|
+
_looksLikeReExport() {
|
|
622
|
+
let i = 1; // start after {
|
|
623
|
+
while (true) {
|
|
624
|
+
const tok = this.peek(i);
|
|
625
|
+
if (!tok || tok.type === TokenType.EOF) return false;
|
|
626
|
+
if (tok.type === TokenType.RBRACE) {
|
|
627
|
+
// After }, must see FROM
|
|
628
|
+
const after = this.peek(i + 1);
|
|
629
|
+
return after && after.type === TokenType.FROM;
|
|
630
|
+
}
|
|
631
|
+
// Inside: expect IDENTIFIER, optionally AS IDENTIFIER, then COMMA or RBRACE
|
|
632
|
+
if (tok.type !== TokenType.IDENTIFIER) return false;
|
|
633
|
+
i++;
|
|
634
|
+
const next = this.peek(i);
|
|
635
|
+
if (next && next.type === TokenType.AS) {
|
|
636
|
+
i++; // skip as
|
|
637
|
+
i++; // skip alias identifier
|
|
638
|
+
}
|
|
639
|
+
const afterId = this.peek(i);
|
|
640
|
+
if (!afterId) return false;
|
|
641
|
+
if (afterId.type === TokenType.COMMA) { i++; continue; }
|
|
642
|
+
if (afterId.type === TokenType.RBRACE) continue;
|
|
643
|
+
return false;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
parseReExport(l) {
|
|
648
|
+
if (this.match(TokenType.STAR)) {
|
|
649
|
+
// pub * from "module"
|
|
650
|
+
this.expect(TokenType.FROM, "Expected 'from' after 'pub *'");
|
|
651
|
+
const source = this.expect(TokenType.STRING, "Expected module path string").value;
|
|
652
|
+
return new AST.ReExportDeclaration(null, source, l);
|
|
653
|
+
}
|
|
654
|
+
// pub { a, b as c } from "module"
|
|
655
|
+
this.expect(TokenType.LBRACE);
|
|
656
|
+
const specifiers = [];
|
|
657
|
+
while (!this.check(TokenType.RBRACE)) {
|
|
658
|
+
const specL = this.loc();
|
|
659
|
+
const imported = this.expect(TokenType.IDENTIFIER, "Expected export name").value;
|
|
660
|
+
let exported = imported;
|
|
661
|
+
if (this.match(TokenType.AS)) {
|
|
662
|
+
exported = this.expect(TokenType.IDENTIFIER, "Expected alias name after 'as'").value;
|
|
663
|
+
}
|
|
664
|
+
specifiers.push(new AST.ReExportSpecifier(imported, exported, specL));
|
|
665
|
+
if (!this.check(TokenType.RBRACE)) {
|
|
666
|
+
this.expect(TokenType.COMMA, "Expected ',' or '}' in re-export list");
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
this.expect(TokenType.RBRACE);
|
|
670
|
+
this.expect(TokenType.FROM, "Expected 'from' after re-export specifiers");
|
|
671
|
+
const source = this.expect(TokenType.STRING, "Expected module path string").value;
|
|
672
|
+
return new AST.ReExportDeclaration(specifiers, source, l);
|
|
673
|
+
}
|
|
674
|
+
|
|
613
675
|
parseImplDeclaration() {
|
|
614
676
|
const l = this.loc();
|
|
615
677
|
this.expect(TokenType.IMPL);
|
|
@@ -1495,23 +1557,56 @@ export class Parser {
|
|
|
1495
1557
|
}
|
|
1496
1558
|
// Destructuring without let: {name, age} = user or [a, b] = list
|
|
1497
1559
|
if (expr.type === 'ObjectLiteral') {
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1560
|
+
let pattern;
|
|
1561
|
+
if (expr._isDestructurePattern) {
|
|
1562
|
+
// Already parsed as a destructuring pattern with defaults
|
|
1563
|
+
pattern = new AST.ObjectPattern(
|
|
1564
|
+
expr.properties.map(p => {
|
|
1565
|
+
const key = typeof p.key === 'string' ? p.key : p.key.name || p.key;
|
|
1566
|
+
const val = p.shorthand ? key
|
|
1567
|
+
: (p.value && p.value.type === 'Identifier' ? p.value.name : key);
|
|
1568
|
+
return { key, value: val, defaultValue: p.defaultValue || null };
|
|
1569
|
+
}),
|
|
1570
|
+
expr.loc
|
|
1571
|
+
);
|
|
1572
|
+
} else {
|
|
1573
|
+
pattern = new AST.ObjectPattern(
|
|
1574
|
+
expr.properties.map(p => {
|
|
1575
|
+
const key = typeof p.key === 'string' ? p.key : p.key.name || p.key;
|
|
1576
|
+
let val, defaultValue = p.defaultValue || null;
|
|
1577
|
+
if (p.shorthand) {
|
|
1578
|
+
// Shorthand with default: { x = 10 } — either via _parseObjectProperty defaultValue or Assignment
|
|
1579
|
+
if (!defaultValue && p.value && p.value.type === 'Assignment' && p.value.targets) {
|
|
1580
|
+
val = key;
|
|
1581
|
+
defaultValue = p.value.values[0];
|
|
1582
|
+
} else {
|
|
1583
|
+
val = key;
|
|
1584
|
+
}
|
|
1585
|
+
} else {
|
|
1586
|
+
// Non-shorthand: { x: y } or { x: y = 10 }
|
|
1587
|
+
if (!defaultValue && p.value && p.value.type === 'Assignment' && p.value.targets) {
|
|
1588
|
+
// { x: y = 10 } — value parsed as Assignment(y, 10)
|
|
1589
|
+
val = p.value.targets[0];
|
|
1590
|
+
if (typeof val !== 'string' && val.name) val = val.name;
|
|
1591
|
+
defaultValue = p.value.values[0];
|
|
1592
|
+
} else if (p.value && p.value.type === 'Identifier') {
|
|
1593
|
+
val = p.value.name;
|
|
1594
|
+
} else {
|
|
1595
|
+
val = key;
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
return { key, value: val, defaultValue };
|
|
1599
|
+
}),
|
|
1600
|
+
expr.loc
|
|
1601
|
+
);
|
|
1602
|
+
}
|
|
1509
1603
|
const value = this.parseExpression();
|
|
1510
1604
|
return new AST.LetDestructure(pattern, value, l);
|
|
1511
1605
|
}
|
|
1512
1606
|
if (expr.type === 'ArrayLiteral') {
|
|
1513
1607
|
const pattern = new AST.ArrayPattern(
|
|
1514
1608
|
expr.elements.map(e => {
|
|
1609
|
+
if (e.type === 'Identifier' && e.name === '_') return null;
|
|
1515
1610
|
if (e.type === 'Identifier') return e.name;
|
|
1516
1611
|
if (e.type === 'SpreadExpression' && e.argument && e.argument.type === 'Identifier') {
|
|
1517
1612
|
return '...' + e.argument.name;
|
|
@@ -1523,11 +1618,24 @@ export class Parser {
|
|
|
1523
1618
|
const value = this.parseExpression();
|
|
1524
1619
|
return new AST.LetDestructure(pattern, value, l);
|
|
1525
1620
|
}
|
|
1621
|
+
// Tuple destructuring without let: (a, b) = expr
|
|
1622
|
+
if (expr.type === 'TupleExpression') {
|
|
1623
|
+
const pattern = new AST.TuplePattern(
|
|
1624
|
+
expr.elements.map(e => {
|
|
1625
|
+
if (e.type === 'Identifier') return e.name;
|
|
1626
|
+
return '_';
|
|
1627
|
+
}),
|
|
1628
|
+
expr.loc
|
|
1629
|
+
);
|
|
1630
|
+
const value = this.parseExpression();
|
|
1631
|
+
return new AST.LetDestructure(pattern, value, l);
|
|
1632
|
+
}
|
|
1526
1633
|
this.error("Invalid assignment target");
|
|
1527
1634
|
}
|
|
1528
1635
|
|
|
1529
|
-
// Compound assignment: x += expr
|
|
1530
|
-
const compoundOp = this.match(TokenType.PLUS_ASSIGN, TokenType.MINUS_ASSIGN, TokenType.STAR_ASSIGN, TokenType.SLASH_ASSIGN
|
|
1636
|
+
// Compound assignment: x += expr, x &= expr, etc.
|
|
1637
|
+
const compoundOp = this.match(TokenType.PLUS_ASSIGN, TokenType.MINUS_ASSIGN, TokenType.STAR_ASSIGN, TokenType.SLASH_ASSIGN,
|
|
1638
|
+
TokenType.BIT_AND_ASSIGN, TokenType.BIT_OR_ASSIGN, TokenType.BIT_XOR_ASSIGN, TokenType.LEFT_SHIFT_ASSIGN, TokenType.RIGHT_SHIFT_ASSIGN);
|
|
1531
1639
|
if (compoundOp) {
|
|
1532
1640
|
if (expr.type !== 'Identifier' && expr.type !== 'MemberExpression') {
|
|
1533
1641
|
this.error("Invalid compound assignment target");
|
|
@@ -1652,7 +1760,46 @@ export class Parser {
|
|
|
1652
1760
|
const operand = this.parseNot();
|
|
1653
1761
|
return new AST.UnaryExpression('not', operand, true, l);
|
|
1654
1762
|
}
|
|
1655
|
-
|
|
1763
|
+
if (this.match(TokenType.TILDE)) {
|
|
1764
|
+
const opTok = this.tokens[this.pos - 1];
|
|
1765
|
+
const l = { line: opTok.line, column: opTok.column, file: this.filename };
|
|
1766
|
+
const operand = this.parseNot();
|
|
1767
|
+
return new AST.UnaryExpression('~', operand, true, l);
|
|
1768
|
+
}
|
|
1769
|
+
return this.parseBitwiseOr();
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
parseBitwiseOr() {
|
|
1773
|
+
let left = this.parseBitwiseXor();
|
|
1774
|
+
while (this.match(TokenType.BAR)) {
|
|
1775
|
+
const opTok = this.tokens[this.pos - 1];
|
|
1776
|
+
const l = { line: opTok.line, column: opTok.column, file: this.filename };
|
|
1777
|
+
const right = this.parseBitwiseXor();
|
|
1778
|
+
left = new AST.BinaryExpression('|', left, right, l);
|
|
1779
|
+
}
|
|
1780
|
+
return left;
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
parseBitwiseXor() {
|
|
1784
|
+
let left = this.parseBitwiseAnd();
|
|
1785
|
+
while (this.match(TokenType.CARET)) {
|
|
1786
|
+
const opTok = this.tokens[this.pos - 1];
|
|
1787
|
+
const l = { line: opTok.line, column: opTok.column, file: this.filename };
|
|
1788
|
+
const right = this.parseBitwiseAnd();
|
|
1789
|
+
left = new AST.BinaryExpression('^', left, right, l);
|
|
1790
|
+
}
|
|
1791
|
+
return left;
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
parseBitwiseAnd() {
|
|
1795
|
+
let left = this.parseComparison();
|
|
1796
|
+
while (this.match(TokenType.AMPERSAND)) {
|
|
1797
|
+
const opTok = this.tokens[this.pos - 1];
|
|
1798
|
+
const l = { line: opTok.line, column: opTok.column, file: this.filename };
|
|
1799
|
+
const right = this.parseComparison();
|
|
1800
|
+
left = new AST.BinaryExpression('&', left, right, l);
|
|
1801
|
+
}
|
|
1802
|
+
return left;
|
|
1656
1803
|
}
|
|
1657
1804
|
|
|
1658
1805
|
parseComparison() {
|
|
@@ -1719,24 +1866,51 @@ export class Parser {
|
|
|
1719
1866
|
}
|
|
1720
1867
|
|
|
1721
1868
|
parseRange() {
|
|
1722
|
-
let left = this.
|
|
1869
|
+
let left = this.parseShift();
|
|
1723
1870
|
|
|
1724
1871
|
if (this.check(TokenType.DOT_DOT_EQUAL)) {
|
|
1725
1872
|
const l = this.loc();
|
|
1726
1873
|
this.advance();
|
|
1727
|
-
const right = this.
|
|
1874
|
+
const right = this.parseShift();
|
|
1728
1875
|
return new AST.RangeExpression(left, right, true, l);
|
|
1729
1876
|
}
|
|
1730
1877
|
if (this.check(TokenType.DOT_DOT)) {
|
|
1731
1878
|
const l = this.loc();
|
|
1732
1879
|
this.advance();
|
|
1733
|
-
const right = this.
|
|
1880
|
+
const right = this.parseShift();
|
|
1734
1881
|
return new AST.RangeExpression(left, right, false, l);
|
|
1735
1882
|
}
|
|
1736
1883
|
|
|
1737
1884
|
return left;
|
|
1738
1885
|
}
|
|
1739
1886
|
|
|
1887
|
+
parseShift() {
|
|
1888
|
+
let left = this.parseAddition();
|
|
1889
|
+
while (true) {
|
|
1890
|
+
const l = this.loc();
|
|
1891
|
+
if (this.match(TokenType.LEFT_SHIFT)) {
|
|
1892
|
+
const right = this.parseAddition();
|
|
1893
|
+
left = new AST.BinaryExpression('<<', left, right, l);
|
|
1894
|
+
} else if (this.check(TokenType.GREATER) && this.peek(1).type === TokenType.GREATER) {
|
|
1895
|
+
// >> or >>> — consume consecutive > tokens in expression context
|
|
1896
|
+
const firstGt = this.advance(); // consume first >
|
|
1897
|
+
if (this.check(TokenType.GREATER) && this.peek(1).type === TokenType.GREATER) {
|
|
1898
|
+
this.advance(); // consume second >
|
|
1899
|
+
this.advance(); // consume third > (completing >>>)
|
|
1900
|
+
const right = this.parseAddition();
|
|
1901
|
+
left = new AST.BinaryExpression('>>>', left, right, { line: firstGt.line, column: firstGt.column, file: this.filename });
|
|
1902
|
+
} else {
|
|
1903
|
+
this.advance(); // consume second > (completing >>)
|
|
1904
|
+
const right = this.parseAddition();
|
|
1905
|
+
left = new AST.BinaryExpression('>>', left, right, { line: firstGt.line, column: firstGt.column, file: this.filename });
|
|
1906
|
+
}
|
|
1907
|
+
} else {
|
|
1908
|
+
break;
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
return left;
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1740
1914
|
parseAddition() {
|
|
1741
1915
|
let left = this.parseMultiplication();
|
|
1742
1916
|
while (true) {
|
|
@@ -2104,7 +2278,8 @@ export class Parser {
|
|
|
2104
2278
|
} else {
|
|
2105
2279
|
// Parse expression, then check for compound/simple assignment
|
|
2106
2280
|
const expr = this.parseExpression();
|
|
2107
|
-
const compoundOp = this.match(TokenType.PLUS_ASSIGN, TokenType.MINUS_ASSIGN, TokenType.STAR_ASSIGN, TokenType.SLASH_ASSIGN
|
|
2281
|
+
const compoundOp = this.match(TokenType.PLUS_ASSIGN, TokenType.MINUS_ASSIGN, TokenType.STAR_ASSIGN, TokenType.SLASH_ASSIGN,
|
|
2282
|
+
TokenType.BIT_AND_ASSIGN, TokenType.BIT_OR_ASSIGN, TokenType.BIT_XOR_ASSIGN, TokenType.LEFT_SHIFT_ASSIGN, TokenType.RIGHT_SHIFT_ASSIGN);
|
|
2108
2283
|
if (compoundOp) {
|
|
2109
2284
|
const value = this.parseExpression();
|
|
2110
2285
|
body = new AST.CompoundAssignment(expr, compoundOp.value, value, l);
|
|
@@ -2347,6 +2522,23 @@ export class Parser {
|
|
|
2347
2522
|
const argument = this.parseUnary();
|
|
2348
2523
|
return { spread: true, argument };
|
|
2349
2524
|
}
|
|
2525
|
+
// Destructuring shorthand with default: x = 10
|
|
2526
|
+
if (this.check(TokenType.IDENTIFIER) && this.peek(1).type === TokenType.ASSIGN) {
|
|
2527
|
+
const key = { type: 'Identifier', name: this.advance().value };
|
|
2528
|
+
this.advance(); // consume =
|
|
2529
|
+
const defaultValue = this.parseExpression();
|
|
2530
|
+
return { key, value: key, shorthand: true, defaultValue };
|
|
2531
|
+
}
|
|
2532
|
+
// Destructuring alias with default: key: alias = expr
|
|
2533
|
+
if (this.check(TokenType.IDENTIFIER) && this.peek(1).type === TokenType.COLON
|
|
2534
|
+
&& this.peek(2).type === TokenType.IDENTIFIER && this.peek(3).type === TokenType.ASSIGN) {
|
|
2535
|
+
const key = { type: 'Identifier', name: this.advance().value };
|
|
2536
|
+
this.advance(); // consume :
|
|
2537
|
+
const alias = { type: 'Identifier', name: this.advance().value };
|
|
2538
|
+
this.advance(); // consume =
|
|
2539
|
+
const defaultValue = this.parseExpression();
|
|
2540
|
+
return { key, value: alias, shorthand: false, defaultValue };
|
|
2541
|
+
}
|
|
2350
2542
|
const key = this.parseExpression();
|
|
2351
2543
|
if (this.match(TokenType.COLON)) {
|
|
2352
2544
|
const value = this.parseExpression();
|
|
@@ -2378,6 +2570,17 @@ export class Parser {
|
|
|
2378
2570
|
return new AST.ObjectLiteral(properties, l);
|
|
2379
2571
|
}
|
|
2380
2572
|
|
|
2573
|
+
// Detect destructuring pattern: { x = val } or { x: y = val }
|
|
2574
|
+
// When IDENTIFIER is followed by ASSIGN, this is a destructuring default, not an object literal
|
|
2575
|
+
if (this.check(TokenType.IDENTIFIER) && this.peek(1).type === TokenType.ASSIGN) {
|
|
2576
|
+
return this._parseObjectPatternAsLiteral(l);
|
|
2577
|
+
}
|
|
2578
|
+
// { x: y = val } — identifier, colon, identifier, assign — destructuring with alias+default
|
|
2579
|
+
if (this.check(TokenType.IDENTIFIER) && this.peek(1).type === TokenType.COLON
|
|
2580
|
+
&& this.peek(2).type === TokenType.IDENTIFIER && this.peek(3).type === TokenType.ASSIGN) {
|
|
2581
|
+
return this._parseObjectPatternAsLiteral(l);
|
|
2582
|
+
}
|
|
2583
|
+
|
|
2381
2584
|
// Try to parse first key: value pair
|
|
2382
2585
|
const firstKey = this.parseExpression();
|
|
2383
2586
|
|
|
@@ -2427,6 +2630,42 @@ export class Parser {
|
|
|
2427
2630
|
this.error("Invalid object literal");
|
|
2428
2631
|
}
|
|
2429
2632
|
|
|
2633
|
+
// Parse object destructuring pattern as an ObjectLiteral-like node for later conversion
|
|
2634
|
+
// Handles: { a = 1, b: y = 2, c } patterns with defaults
|
|
2635
|
+
// Note: the opening { has already been consumed by parseObjectOrDictComprehension
|
|
2636
|
+
_parseObjectPatternAsLiteral(l) {
|
|
2637
|
+
const properties = [];
|
|
2638
|
+
|
|
2639
|
+
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
|
2640
|
+
const key = this.expect(TokenType.IDENTIFIER, "Expected property name").value;
|
|
2641
|
+
let valueName = key;
|
|
2642
|
+
let defaultValue = null;
|
|
2643
|
+
|
|
2644
|
+
if (this.match(TokenType.COLON)) {
|
|
2645
|
+
valueName = this.expect(TokenType.IDENTIFIER, "Expected alias name").value;
|
|
2646
|
+
}
|
|
2647
|
+
if (this.match(TokenType.ASSIGN)) {
|
|
2648
|
+
defaultValue = this.parseExpression();
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2651
|
+
properties.push({
|
|
2652
|
+
key: { type: 'Identifier', name: key },
|
|
2653
|
+
value: { type: 'Identifier', name: valueName },
|
|
2654
|
+
shorthand: key === valueName,
|
|
2655
|
+
defaultValue: defaultValue
|
|
2656
|
+
});
|
|
2657
|
+
if (!this.match(TokenType.COMMA)) break;
|
|
2658
|
+
}
|
|
2659
|
+
|
|
2660
|
+
this.expect(TokenType.RBRACE, "Expected '}' in object pattern");
|
|
2661
|
+
return {
|
|
2662
|
+
type: 'ObjectLiteral',
|
|
2663
|
+
properties,
|
|
2664
|
+
loc: l,
|
|
2665
|
+
_isDestructurePattern: true
|
|
2666
|
+
};
|
|
2667
|
+
}
|
|
2668
|
+
|
|
2430
2669
|
parseParenOrArrowLambda() {
|
|
2431
2670
|
const l = this.loc();
|
|
2432
2671
|
|
package/src/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Auto-generated by scripts/embed-runtime.js — do not edit
|
|
2
|
-
export const VERSION = "0.9.
|
|
2
|
+
export const VERSION = "0.9.6";
|