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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tova",
3
- "version": "0.9.5",
3
+ "version": "0.9.6",
4
4
  "description": "Tova — a modern programming language that transpiles to JavaScript, unifying frontend and backend",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -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 in strict mode
2115
- if (this.strict && newType === 'Float' && existing.inferredType === 'Int') {
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})`;
@@ -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 - should not reach here normally
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.error(`Unexpected character: '&'. Did you mean '&&'?`);
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
  }
@@ -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', 'let', 'if', 'elif', 'else', 'for', 'while', 'loop', 'when', 'in',
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
  // ============================================================
@@ -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)) return this.parseLetDestructure();
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
- const pattern = new AST.ObjectPattern(
1499
- expr.properties.map(p => {
1500
- const key = typeof p.key === 'string' ? p.key : p.key.name || p.key;
1501
- // For shorthand {name}, key and value are the same
1502
- // For rename {name: alias}, value is the alias identifier
1503
- const val = p.shorthand ? key
1504
- : (p.value && p.value.type === 'Identifier' ? p.value.name : key);
1505
- return { key, value: val };
1506
- }),
1507
- expr.loc
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
- return this.parseComparison();
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.parseAddition();
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.parseAddition();
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.parseAddition();
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.5";
2
+ export const VERSION = "0.9.6";