tova 0.7.0 → 0.9.4
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 +1312 -139
- package/package.json +8 -1
- package/src/analyzer/analyzer.js +539 -11
- package/src/analyzer/browser-analyzer.js +56 -8
- package/src/analyzer/deploy-analyzer.js +44 -0
- package/src/analyzer/scope.js +7 -0
- package/src/analyzer/server-analyzer.js +33 -1
- package/src/codegen/base-codegen.js +1296 -23
- package/src/codegen/browser-codegen.js +725 -20
- package/src/codegen/codegen.js +87 -5
- package/src/codegen/deploy-codegen.js +49 -0
- package/src/codegen/server-codegen.js +54 -6
- package/src/codegen/shared-codegen.js +5 -0
- package/src/codegen/theme-codegen.js +69 -0
- package/src/codegen/wasm-codegen.js +6 -0
- package/src/config/edit-toml.js +6 -2
- package/src/config/git-resolver.js +128 -0
- package/src/config/lock-file.js +57 -0
- package/src/config/module-cache.js +58 -0
- package/src/config/module-entry.js +37 -0
- package/src/config/module-path.js +63 -0
- package/src/config/pkg-errors.js +62 -0
- package/src/config/resolve.js +26 -0
- package/src/config/resolver.js +139 -0
- package/src/config/search.js +28 -0
- package/src/config/semver.js +72 -0
- package/src/config/toml.js +61 -6
- package/src/deploy/deploy.js +217 -0
- package/src/deploy/infer.js +218 -0
- package/src/deploy/provision.js +315 -0
- package/src/diagnostics/security-scorecard.js +111 -0
- package/src/lexer/lexer.js +18 -3
- package/src/lsp/server.js +482 -0
- package/src/parser/animate-ast.js +45 -0
- package/src/parser/ast.js +39 -0
- package/src/parser/browser-ast.js +19 -1
- package/src/parser/browser-parser.js +221 -4
- package/src/parser/concurrency-ast.js +15 -0
- package/src/parser/concurrency-parser.js +236 -0
- package/src/parser/deploy-ast.js +37 -0
- package/src/parser/deploy-parser.js +132 -0
- package/src/parser/parser.js +42 -5
- package/src/parser/select-ast.js +39 -0
- package/src/parser/theme-ast.js +29 -0
- package/src/parser/theme-parser.js +70 -0
- package/src/registry/plugins/concurrency-plugin.js +32 -0
- package/src/registry/plugins/deploy-plugin.js +33 -0
- package/src/registry/plugins/theme-plugin.js +20 -0
- package/src/registry/register-all.js +6 -0
- package/src/runtime/charts.js +547 -0
- package/src/runtime/embedded.js +6 -2
- package/src/runtime/reactivity.js +60 -0
- package/src/runtime/router.js +703 -295
- package/src/runtime/table.js +606 -33
- package/src/stdlib/inline.js +365 -10
- package/src/stdlib/runtime-bridge.js +152 -0
- package/src/stdlib/string.js +84 -2
- package/src/stdlib/validation.js +1 -1
- package/src/version.js +1 -1
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
import { TokenType, Keywords } from '../lexer/tokens.js';
|
|
5
5
|
import * as AST from './ast.js';
|
|
6
6
|
import { installFormParser } from './form-parser.js';
|
|
7
|
+
import {
|
|
8
|
+
AnimateDeclaration, AnimatePrimitive, AnimateSequence, AnimateParallel,
|
|
9
|
+
} from './animate-ast.js';
|
|
10
|
+
import { FontDeclaration } from './browser-ast.js';
|
|
7
11
|
|
|
8
12
|
export function installBrowserParser(ParserClass) {
|
|
9
13
|
if (ParserClass.prototype._browserParserInstalled) return;
|
|
@@ -112,7 +116,17 @@ export function installBrowserParser(ParserClass) {
|
|
|
112
116
|
ParserClass.prototype.parseComponent = function() {
|
|
113
117
|
const l = this.loc();
|
|
114
118
|
this.expect(TokenType.COMPONENT);
|
|
115
|
-
|
|
119
|
+
let name = this.expect(TokenType.IDENTIFIER, "Expected component name").value;
|
|
120
|
+
|
|
121
|
+
// Check for compound component: Dialog.Title
|
|
122
|
+
let parent = null;
|
|
123
|
+
let child = null;
|
|
124
|
+
if (this.check(TokenType.DOT)) {
|
|
125
|
+
this.advance(); // consume '.'
|
|
126
|
+
child = this.expect(TokenType.IDENTIFIER, "Expected sub-component name after '.'").value;
|
|
127
|
+
parent = name;
|
|
128
|
+
name = parent + '.' + child;
|
|
129
|
+
}
|
|
116
130
|
|
|
117
131
|
let params = [];
|
|
118
132
|
if (this.match(TokenType.LPAREN)) {
|
|
@@ -125,9 +139,23 @@ export function installBrowserParser(ParserClass) {
|
|
|
125
139
|
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
|
126
140
|
if (this.check(TokenType.STYLE_BLOCK)) {
|
|
127
141
|
const sl = this.loc();
|
|
128
|
-
|
|
142
|
+
let css = this.current().value;
|
|
143
|
+
let config = null;
|
|
144
|
+
// Parse __CONFIG:key:value__ prefix from lexer
|
|
145
|
+
if (css.startsWith('__CONFIG:')) {
|
|
146
|
+
const endIdx = css.indexOf('__', 9);
|
|
147
|
+
if (endIdx !== -1) {
|
|
148
|
+
const configStr = css.slice(9, endIdx);
|
|
149
|
+
config = {};
|
|
150
|
+
for (const part of configStr.split(',')) {
|
|
151
|
+
const [k, v] = part.split(':').map(s => s.trim());
|
|
152
|
+
if (k) config[k] = v || true;
|
|
153
|
+
}
|
|
154
|
+
css = css.slice(endIdx + 2).trim();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
129
157
|
this.advance();
|
|
130
|
-
body.push(new AST.ComponentStyleBlock(css, sl));
|
|
158
|
+
body.push(new AST.ComponentStyleBlock(css, sl, config));
|
|
131
159
|
} else if (this.check(TokenType.LESS) && this._looksLikeJSX()) {
|
|
132
160
|
body.push(this.parseJSXElementOrFragment());
|
|
133
161
|
} else if (this.check(TokenType.STATE)) {
|
|
@@ -140,13 +168,22 @@ export function installBrowserParser(ParserClass) {
|
|
|
140
168
|
body.push(this.parseComponent());
|
|
141
169
|
} else if (this.check(TokenType.FORM)) {
|
|
142
170
|
body.push(this.parseFormDeclaration());
|
|
171
|
+
} else if (this.check(TokenType.IDENTIFIER) && this.current().value === 'font' && this.peek(1).type === TokenType.IDENTIFIER && this.peek(2).type === TokenType.FROM) {
|
|
172
|
+
body.push(this.parseComponentFontDeclaration());
|
|
173
|
+
} else if (this.check(TokenType.IDENTIFIER) && this.current().value === 'animate' && this.peek(1).type === TokenType.IDENTIFIER && this.peek(2).type === TokenType.LBRACE) {
|
|
174
|
+
body.push(this.parseAnimateDeclaration());
|
|
143
175
|
} else {
|
|
144
176
|
body.push(this.parseStatement());
|
|
145
177
|
}
|
|
146
178
|
}
|
|
147
179
|
this.expect(TokenType.RBRACE, "Expected '}' to close component body");
|
|
148
180
|
|
|
149
|
-
|
|
181
|
+
const node = new AST.ComponentDeclaration(name, params, body, l);
|
|
182
|
+
if (parent) {
|
|
183
|
+
node.parent = parent;
|
|
184
|
+
node.child = child;
|
|
185
|
+
}
|
|
186
|
+
return node;
|
|
150
187
|
};
|
|
151
188
|
|
|
152
189
|
// ─── JSX-like parsing ─────────────────────────────────────
|
|
@@ -305,6 +342,13 @@ export function installBrowserParser(ParserClass) {
|
|
|
305
342
|
this.error("Expected attribute name");
|
|
306
343
|
}
|
|
307
344
|
|
|
345
|
+
// Handle hyphenated attribute names: aria-disabled, data-testid, stroke-width, etc.
|
|
346
|
+
while (this.check(TokenType.MINUS) && this.peek(1) &&
|
|
347
|
+
(this.peek(1).type === TokenType.IDENTIFIER || this.peek(1).value in Keywords)) {
|
|
348
|
+
this.advance(); // consume MINUS
|
|
349
|
+
name += '-' + this.advance().value;
|
|
350
|
+
}
|
|
351
|
+
|
|
308
352
|
// Handle namespaced attributes: on:click, bind:value, class:active
|
|
309
353
|
if (this.match(TokenType.COLON)) {
|
|
310
354
|
let suffix;
|
|
@@ -608,4 +652,177 @@ export function installBrowserParser(ParserClass) {
|
|
|
608
652
|
this.expect(TokenType.RBRACE, "Expected '}' to close JSX match body");
|
|
609
653
|
return new AST.JSXMatch(subject, arms, l);
|
|
610
654
|
};
|
|
655
|
+
|
|
656
|
+
// ─── Font declaration parsing ──────────────────────────────
|
|
657
|
+
|
|
658
|
+
ParserClass.prototype.parseComponentFontDeclaration = function() {
|
|
659
|
+
const l = this.loc();
|
|
660
|
+
this.advance(); // consume 'font' identifier
|
|
661
|
+
const name = this.expect(TokenType.IDENTIFIER, "Expected font name after 'font'").value;
|
|
662
|
+
this.expect(TokenType.FROM, "Expected 'from' after font name");
|
|
663
|
+
const source = this.expect(TokenType.STRING, "Expected URL or path string after 'from'").value;
|
|
664
|
+
|
|
665
|
+
// Optional config block: { weight: "400" style: "normal" display: "swap" }
|
|
666
|
+
let config = null;
|
|
667
|
+
if (this.check(TokenType.LBRACE)) {
|
|
668
|
+
this.advance(); // consume '{'
|
|
669
|
+
config = {};
|
|
670
|
+
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
|
671
|
+
const key = this.expect(TokenType.IDENTIFIER, "Expected config key (weight, style, display)").value;
|
|
672
|
+
this.expect(TokenType.COLON, `Expected ':' after '${key}'`);
|
|
673
|
+
const val = this.expect(TokenType.STRING, `Expected string value for '${key}'`).value;
|
|
674
|
+
config[key] = val;
|
|
675
|
+
}
|
|
676
|
+
this.expect(TokenType.RBRACE, "Expected '}' to close font config block");
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
return new FontDeclaration(name, source, config, l);
|
|
680
|
+
};
|
|
681
|
+
|
|
682
|
+
// ─── Animate block parsing ─────────────────────────────────
|
|
683
|
+
|
|
684
|
+
ParserClass.prototype.parseAnimateDeclaration = function() {
|
|
685
|
+
const l = this.loc();
|
|
686
|
+
this.advance(); // consume 'animate' identifier
|
|
687
|
+
const name = this.expect(TokenType.IDENTIFIER, "Expected animation name after 'animate'").value;
|
|
688
|
+
this.expect(TokenType.LBRACE, "Expected '{' after animate name");
|
|
689
|
+
|
|
690
|
+
let enter = null;
|
|
691
|
+
let exit = null;
|
|
692
|
+
let duration = null;
|
|
693
|
+
let easing = null;
|
|
694
|
+
let stagger = null;
|
|
695
|
+
let stay = null;
|
|
696
|
+
|
|
697
|
+
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
|
698
|
+
const key = this.expect(TokenType.IDENTIFIER, "Expected property name inside animate block").value;
|
|
699
|
+
this.expect(TokenType.COLON, `Expected ':' after '${key}'`);
|
|
700
|
+
|
|
701
|
+
switch (key) {
|
|
702
|
+
case 'enter':
|
|
703
|
+
enter = this._parseAnimateComposition();
|
|
704
|
+
break;
|
|
705
|
+
case 'exit':
|
|
706
|
+
exit = this._parseAnimateComposition();
|
|
707
|
+
break;
|
|
708
|
+
case 'duration': {
|
|
709
|
+
const tok = this.expect(TokenType.NUMBER, "Expected number for 'duration'");
|
|
710
|
+
duration = Number(tok.value);
|
|
711
|
+
break;
|
|
712
|
+
}
|
|
713
|
+
case 'easing': {
|
|
714
|
+
const tok = this.expect(TokenType.STRING, "Expected string for 'easing'");
|
|
715
|
+
easing = tok.value;
|
|
716
|
+
break;
|
|
717
|
+
}
|
|
718
|
+
case 'stagger': {
|
|
719
|
+
const tok = this.expect(TokenType.NUMBER, "Expected number for 'stagger'");
|
|
720
|
+
stagger = Number(tok.value);
|
|
721
|
+
break;
|
|
722
|
+
}
|
|
723
|
+
case 'stay': {
|
|
724
|
+
const tok = this.expect(TokenType.NUMBER, "Expected number for 'stay'");
|
|
725
|
+
stay = Number(tok.value);
|
|
726
|
+
break;
|
|
727
|
+
}
|
|
728
|
+
default:
|
|
729
|
+
this.error(`Unknown animate property '${key}'. Expected enter, exit, duration, easing, stagger, or stay`);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
this.expect(TokenType.RBRACE, "Expected '}' to close animate block");
|
|
734
|
+
return new AnimateDeclaration(name, enter, exit, duration, easing, stagger, stay, l);
|
|
735
|
+
};
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* Parse animate composition with precedence:
|
|
739
|
+
* - `then` has lowest precedence (creates AnimateSequence)
|
|
740
|
+
* - `+` has higher precedence (creates AnimateParallel)
|
|
741
|
+
*/
|
|
742
|
+
ParserClass.prototype._parseAnimateComposition = function() {
|
|
743
|
+
const l = this.loc();
|
|
744
|
+
let left = this._parseAnimateParallel();
|
|
745
|
+
|
|
746
|
+
// Check for `then` (IDENTIFIER with value 'then') — sequential composition
|
|
747
|
+
const parts = [left];
|
|
748
|
+
while (this.check(TokenType.IDENTIFIER) && this.current().value === 'then') {
|
|
749
|
+
this.advance(); // consume 'then'
|
|
750
|
+
parts.push(this._parseAnimateParallel());
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
if (parts.length === 1) return left;
|
|
754
|
+
return new AnimateSequence(parts, l);
|
|
755
|
+
};
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* Parse parallel composition: primitives joined with `+`
|
|
759
|
+
*/
|
|
760
|
+
ParserClass.prototype._parseAnimateParallel = function() {
|
|
761
|
+
const l = this.loc();
|
|
762
|
+
let left = this._parseAnimatePrimitive();
|
|
763
|
+
|
|
764
|
+
const parts = [left];
|
|
765
|
+
while (this.check(TokenType.PLUS)) {
|
|
766
|
+
this.advance(); // consume '+'
|
|
767
|
+
parts.push(this._parseAnimatePrimitive());
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
if (parts.length === 1) return left;
|
|
771
|
+
return new AnimateParallel(parts, l);
|
|
772
|
+
};
|
|
773
|
+
|
|
774
|
+
/**
|
|
775
|
+
* Parse a single animation primitive: name(key: value, key: value)
|
|
776
|
+
* Supports parenthesized grouping: (expr)
|
|
777
|
+
*/
|
|
778
|
+
ParserClass.prototype._parseAnimatePrimitive = function() {
|
|
779
|
+
// Parenthesized grouping for precedence override
|
|
780
|
+
if (this.check(TokenType.LPAREN)) {
|
|
781
|
+
this.advance(); // consume '('
|
|
782
|
+
const inner = this._parseAnimateComposition();
|
|
783
|
+
this.expect(TokenType.RPAREN, "Expected ')' after grouped animation expression");
|
|
784
|
+
return inner;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
const l = this.loc();
|
|
788
|
+
const primName = this.expect(TokenType.IDENTIFIER, "Expected animation primitive name (fade, slide, scale, rotate, blur)").value;
|
|
789
|
+
this.expect(TokenType.LPAREN, `Expected '(' after '${primName}'`);
|
|
790
|
+
|
|
791
|
+
const params = {};
|
|
792
|
+
while (!this.check(TokenType.RPAREN) && !this.isAtEnd()) {
|
|
793
|
+
// Accept identifiers and keywords as parameter names (e.g., 'from' is a keyword in Tova)
|
|
794
|
+
let paramKey;
|
|
795
|
+
if (this.check(TokenType.IDENTIFIER) || (typeof this.current().value === 'string' && this.current().value in Keywords)) {
|
|
796
|
+
paramKey = this.advance().value;
|
|
797
|
+
} else {
|
|
798
|
+
paramKey = this.expect(TokenType.IDENTIFIER, "Expected parameter name").value;
|
|
799
|
+
}
|
|
800
|
+
this.expect(TokenType.COLON, `Expected ':' after parameter name '${paramKey}'`);
|
|
801
|
+
|
|
802
|
+
// Value can be a number (including negative), identifier, or string
|
|
803
|
+
if (this.check(TokenType.MINUS)) {
|
|
804
|
+
this.advance(); // consume '-'
|
|
805
|
+
const tok = this.expect(TokenType.NUMBER, "Expected number after '-'");
|
|
806
|
+
params[paramKey] = -Number(tok.value);
|
|
807
|
+
} else if (this.check(TokenType.NUMBER)) {
|
|
808
|
+
const tok = this.advance();
|
|
809
|
+
params[paramKey] = Number(tok.value);
|
|
810
|
+
} else if (this.check(TokenType.STRING)) {
|
|
811
|
+
const tok = this.advance();
|
|
812
|
+
params[paramKey] = tok.value;
|
|
813
|
+
} else if (this.check(TokenType.IDENTIFIER)) {
|
|
814
|
+
const tok = this.advance();
|
|
815
|
+
params[paramKey] = tok.value;
|
|
816
|
+
} else {
|
|
817
|
+
this.error("Expected number, string, or identifier as animation parameter value");
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
if (!this.check(TokenType.RPAREN)) {
|
|
821
|
+
this.expect(TokenType.COMMA, "Expected ',' between animation parameters");
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
this.expect(TokenType.RPAREN, `Expected ')' to close '${primName}' parameters`);
|
|
826
|
+
return new AnimatePrimitive(primName, params, l);
|
|
827
|
+
};
|
|
611
828
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Concurrency-specific AST Node definitions for the Tova language
|
|
2
|
+
// Extracted for lazy loading -- only loaded when concurrent { } blocks are used.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* spawn foo(args)
|
|
6
|
+
* spawn fn() { ... }
|
|
7
|
+
*/
|
|
8
|
+
export class SpawnExpression {
|
|
9
|
+
constructor(callee, args, loc) {
|
|
10
|
+
this.type = 'SpawnExpression';
|
|
11
|
+
this.callee = callee; // Expression (function name or lambda)
|
|
12
|
+
this.arguments = args; // Array of Expression
|
|
13
|
+
this.loc = loc;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
// Concurrency-specific parser methods for the Tova language
|
|
2
|
+
// Extracted for lazy loading — only loaded when concurrent { } blocks are used.
|
|
3
|
+
|
|
4
|
+
import { TokenType } from '../lexer/tokens.js';
|
|
5
|
+
import * as AST from './ast.js';
|
|
6
|
+
import { SpawnExpression } from './concurrency-ast.js';
|
|
7
|
+
import { SelectStatement, SelectCase } from './select-ast.js';
|
|
8
|
+
|
|
9
|
+
const CONCURRENT_MODES = new Set(['cancel_on_error', 'first', 'timeout']);
|
|
10
|
+
|
|
11
|
+
export function installConcurrencyParser(ParserClass) {
|
|
12
|
+
if (ParserClass.prototype._concurrencyParserInstalled) return;
|
|
13
|
+
ParserClass.prototype._concurrencyParserInstalled = true;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Parse: concurrent [mode] { body }
|
|
17
|
+
*
|
|
18
|
+
* Modes:
|
|
19
|
+
* concurrent { ... } — mode "all" (default)
|
|
20
|
+
* concurrent cancel_on_error { ... } — cancel siblings on first error
|
|
21
|
+
* concurrent first { ... } — return first result, cancel rest
|
|
22
|
+
* concurrent timeout(ms) { ... } — timeout after ms milliseconds
|
|
23
|
+
*/
|
|
24
|
+
ParserClass.prototype.parseConcurrentBlock = function() {
|
|
25
|
+
const l = this.loc();
|
|
26
|
+
this.advance(); // consume 'concurrent'
|
|
27
|
+
|
|
28
|
+
let mode = 'all';
|
|
29
|
+
let timeout = null;
|
|
30
|
+
|
|
31
|
+
// Check for mode modifier
|
|
32
|
+
if (this.check(TokenType.IDENTIFIER) && CONCURRENT_MODES.has(this.current().value)) {
|
|
33
|
+
const modeName = this.advance().value;
|
|
34
|
+
if (modeName === 'timeout') {
|
|
35
|
+
this.expect(TokenType.LPAREN, "Expected '(' after 'timeout'");
|
|
36
|
+
timeout = this.parseExpression();
|
|
37
|
+
this.expect(TokenType.RPAREN, "Expected ')' after timeout value");
|
|
38
|
+
mode = 'timeout';
|
|
39
|
+
} else {
|
|
40
|
+
mode = modeName;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
this.expect(TokenType.LBRACE, "Expected '{' after 'concurrent'");
|
|
45
|
+
|
|
46
|
+
const body = [];
|
|
47
|
+
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
|
48
|
+
try {
|
|
49
|
+
const stmt = this.parseStatement();
|
|
50
|
+
if (stmt) body.push(stmt);
|
|
51
|
+
} catch (e) {
|
|
52
|
+
this.errors.push(e);
|
|
53
|
+
this._synchronizeBlock();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
this.expect(TokenType.RBRACE, "Expected '}' to close concurrent block");
|
|
58
|
+
return new AST.ConcurrentBlock(mode, timeout, body, l);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// Save the original parseUnary method to extend it with spawn support
|
|
62
|
+
const _originalParseUnary = ParserClass.prototype.parseUnary;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Extend parseUnary to handle `spawn` as a prefix expression.
|
|
66
|
+
* spawn foo(args) → SpawnExpression
|
|
67
|
+
* Works like `await` but for concurrent task spawning.
|
|
68
|
+
*/
|
|
69
|
+
ParserClass.prototype.parseUnary = function() {
|
|
70
|
+
if (this.check(TokenType.IDENTIFIER) && this.current().value === 'spawn') {
|
|
71
|
+
// Distinguish concurrency `spawn foo()` from stdlib function call `spawn("cmd", args)`.
|
|
72
|
+
// If `spawn` is followed by `(`, it's a regular function call, not a concurrency keyword.
|
|
73
|
+
const next = this.peek(1);
|
|
74
|
+
if (next && next.type === TokenType.LPAREN) {
|
|
75
|
+
return _originalParseUnary.call(this);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const l = this.loc();
|
|
79
|
+
this.advance(); // consume 'spawn'
|
|
80
|
+
|
|
81
|
+
// Parse the expression after spawn (function call, lambda, etc.)
|
|
82
|
+
const expr = this.parseUnary();
|
|
83
|
+
|
|
84
|
+
// If it's a call expression, split into callee + args
|
|
85
|
+
if (expr.type === 'CallExpression') {
|
|
86
|
+
return new SpawnExpression(expr.callee, expr.arguments, l);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Otherwise treat the whole expression as the callee with no args
|
|
90
|
+
return new SpawnExpression(expr, [], l);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return _originalParseUnary.call(this);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// Also support concurrent as a statement inside function bodies
|
|
97
|
+
const _originalParseStatement = ParserClass.prototype.parseStatement;
|
|
98
|
+
|
|
99
|
+
ParserClass.prototype.parseStatement = function() {
|
|
100
|
+
// Check for 'concurrent' at statement level (inside function bodies)
|
|
101
|
+
if (this.check(TokenType.IDENTIFIER) && this.current().value === 'concurrent') {
|
|
102
|
+
return this.parseConcurrentBlock();
|
|
103
|
+
}
|
|
104
|
+
// Check for 'select {' at statement level (disambiguate from select() function call)
|
|
105
|
+
if (this.check(TokenType.IDENTIFIER) && this.current().value === 'select'
|
|
106
|
+
&& this.peek(1).type === TokenType.LBRACE) {
|
|
107
|
+
return this.parseSelectStatement();
|
|
108
|
+
}
|
|
109
|
+
return _originalParseStatement.call(this);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Parse: select { case1 case2 ... }
|
|
114
|
+
*
|
|
115
|
+
* Each case is one of:
|
|
116
|
+
* binding from channel => body
|
|
117
|
+
* _ from channel => body
|
|
118
|
+
* channel.send(value) => body
|
|
119
|
+
* timeout(ms) => body
|
|
120
|
+
* _ => body
|
|
121
|
+
*/
|
|
122
|
+
ParserClass.prototype.parseSelectStatement = function() {
|
|
123
|
+
const l = this.loc();
|
|
124
|
+
this.advance(); // consume 'select'
|
|
125
|
+
this.expect(TokenType.LBRACE, "Expected '{' after 'select'");
|
|
126
|
+
|
|
127
|
+
const cases = [];
|
|
128
|
+
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
|
129
|
+
try {
|
|
130
|
+
cases.push(this.parseSelectCase());
|
|
131
|
+
} catch (e) {
|
|
132
|
+
this.errors.push(e);
|
|
133
|
+
this._synchronizeBlock();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
this.expect(TokenType.RBRACE, "Expected '}' to close select block");
|
|
138
|
+
return new SelectStatement(cases, l);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
ParserClass.prototype.parseSelectCase = function() {
|
|
142
|
+
const l = this.loc();
|
|
143
|
+
|
|
144
|
+
// timeout(ms) => body
|
|
145
|
+
if (this.check(TokenType.IDENTIFIER) && this.current().value === 'timeout'
|
|
146
|
+
&& this.peek(1).type === TokenType.LPAREN) {
|
|
147
|
+
this.advance(); // consume 'timeout'
|
|
148
|
+
this.expect(TokenType.LPAREN, "Expected '(' after 'timeout'");
|
|
149
|
+
const ms = this.parseExpression();
|
|
150
|
+
this.expect(TokenType.RPAREN, "Expected ')' after timeout value");
|
|
151
|
+
this.expect(TokenType.ARROW, "Expected '=>' after timeout");
|
|
152
|
+
const body = this.parseSelectCaseBody();
|
|
153
|
+
return new SelectCase('timeout', null, null, ms, body, l);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// _ => body (default case) — must check before _ from channel
|
|
157
|
+
if (this.check(TokenType.IDENTIFIER) && this.current().value === '_'
|
|
158
|
+
&& this.peek(1).type === TokenType.ARROW) {
|
|
159
|
+
this.advance(); // consume '_'
|
|
160
|
+
this.expect(TokenType.ARROW, "Expected '=>' after '_'");
|
|
161
|
+
const body = this.parseSelectCaseBody();
|
|
162
|
+
return new SelectCase('default', null, null, null, body, l);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// _ from channel => body (wildcard receive)
|
|
166
|
+
if (this.check(TokenType.IDENTIFIER) && this.current().value === '_'
|
|
167
|
+
&& this.peek(1).type === TokenType.FROM) {
|
|
168
|
+
this.advance(); // consume '_'
|
|
169
|
+
this.advance(); // consume 'from'
|
|
170
|
+
const channel = this._parseSelectChannel();
|
|
171
|
+
this.expect(TokenType.ARROW, "Expected '=>' after channel");
|
|
172
|
+
const body = this.parseSelectCaseBody();
|
|
173
|
+
return new SelectCase('receive', channel, null, null, body, l);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// binding from channel => body (named receive)
|
|
177
|
+
if (this.check(TokenType.IDENTIFIER) && this.peek(1).type === TokenType.FROM) {
|
|
178
|
+
const binding = this.advance().value; // consume binding name
|
|
179
|
+
this.advance(); // consume 'from'
|
|
180
|
+
const channel = this._parseSelectChannel();
|
|
181
|
+
this.expect(TokenType.ARROW, "Expected '=>' after channel");
|
|
182
|
+
const body = this.parseSelectCaseBody();
|
|
183
|
+
return new SelectCase('receive', channel, binding, null, body, l);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// channel.send(value) => body (send case)
|
|
187
|
+
// Parse as expression, then check if it's a send call
|
|
188
|
+
const expr = this.parseExpression();
|
|
189
|
+
if (expr.type === 'CallExpression' && expr.callee.type === 'MemberExpression'
|
|
190
|
+
&& expr.callee.property === 'send') {
|
|
191
|
+
this.expect(TokenType.ARROW, "Expected '=>' after send");
|
|
192
|
+
const body = this.parseSelectCaseBody();
|
|
193
|
+
return new SelectCase('send', expr.callee.object, null, expr.arguments[0], body, l);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
throw this.error("Expected select case: 'binding from channel =>', 'timeout(ms) =>', 'channel.send(val) =>', or '_ =>'", l);
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Parse a channel expression in a select receive case.
|
|
201
|
+
* Handles identifiers and member access chains (e.g., ch, obj.ch)
|
|
202
|
+
* without consuming '=>' as a lambda arrow.
|
|
203
|
+
*/
|
|
204
|
+
ParserClass.prototype._parseSelectChannel = function() {
|
|
205
|
+
const l = this.loc();
|
|
206
|
+
let expr = new AST.Identifier(this.advance().value, l);
|
|
207
|
+
// Follow member access chains: ch.sub, obj.channels, etc.
|
|
208
|
+
while (this.check(TokenType.DOT)) {
|
|
209
|
+
this.advance(); // consume '.'
|
|
210
|
+
const prop = this.advance().value;
|
|
211
|
+
expr = new AST.MemberExpression(expr, prop, false, this.loc());
|
|
212
|
+
}
|
|
213
|
+
return expr;
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
ParserClass.prototype.parseSelectCaseBody = function() {
|
|
217
|
+
if (this.check(TokenType.LBRACE)) {
|
|
218
|
+
this.advance(); // consume '{'
|
|
219
|
+
const body = [];
|
|
220
|
+
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
|
221
|
+
try {
|
|
222
|
+
const stmt = this.parseStatement();
|
|
223
|
+
if (stmt) body.push(stmt);
|
|
224
|
+
} catch (e) {
|
|
225
|
+
this.errors.push(e);
|
|
226
|
+
this._synchronizeBlock();
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
this.expect(TokenType.RBRACE, "Expected '}' to close select case body");
|
|
230
|
+
return body;
|
|
231
|
+
}
|
|
232
|
+
// Single statement
|
|
233
|
+
const stmt = this.parseStatement();
|
|
234
|
+
return stmt ? [stmt] : [];
|
|
235
|
+
};
|
|
236
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Deploy-specific AST Node definitions for the Tova language
|
|
2
|
+
// Extracted for lazy loading — only loaded when deploy { } blocks are used.
|
|
3
|
+
|
|
4
|
+
export class DeployBlock {
|
|
5
|
+
constructor(body, loc, name = null) {
|
|
6
|
+
this.type = 'DeployBlock';
|
|
7
|
+
this.name = name;
|
|
8
|
+
this.body = body;
|
|
9
|
+
this.loc = loc;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class DeployConfigField {
|
|
14
|
+
constructor(key, value, loc) {
|
|
15
|
+
this.type = 'DeployConfigField';
|
|
16
|
+
this.key = key;
|
|
17
|
+
this.value = value;
|
|
18
|
+
this.loc = loc;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class DeployEnvBlock {
|
|
23
|
+
constructor(entries, loc) {
|
|
24
|
+
this.type = 'DeployEnvBlock';
|
|
25
|
+
this.entries = entries;
|
|
26
|
+
this.loc = loc;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class DeployDbBlock {
|
|
31
|
+
constructor(engine, config, loc) {
|
|
32
|
+
this.type = 'DeployDbBlock';
|
|
33
|
+
this.engine = engine;
|
|
34
|
+
this.config = config;
|
|
35
|
+
this.loc = loc;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// Deploy-specific parser methods for the Tova language
|
|
2
|
+
// Extracted from parser.js for lazy loading — only loaded when deploy { } blocks are encountered.
|
|
3
|
+
|
|
4
|
+
import { TokenType } from '../lexer/tokens.js';
|
|
5
|
+
import {
|
|
6
|
+
DeployBlock, DeployConfigField, DeployEnvBlock, DeployDbBlock,
|
|
7
|
+
} from './deploy-ast.js';
|
|
8
|
+
|
|
9
|
+
// Keywords that start sub-blocks (not config fields)
|
|
10
|
+
const DEPLOY_SUB_BLOCK_KEYWORDS = new Set(['env', 'db']);
|
|
11
|
+
|
|
12
|
+
export function installDeployParser(ParserClass) {
|
|
13
|
+
if (ParserClass.prototype._deployParserInstalled) return;
|
|
14
|
+
ParserClass.prototype._deployParserInstalled = true;
|
|
15
|
+
|
|
16
|
+
ParserClass.prototype.parseDeployBlock = function() {
|
|
17
|
+
const l = this.loc();
|
|
18
|
+
this.advance(); // consume 'deploy'
|
|
19
|
+
|
|
20
|
+
// Deploy blocks REQUIRE a name
|
|
21
|
+
if (!this.check(TokenType.STRING)) {
|
|
22
|
+
throw this.error("Deploy block requires a name (e.g., deploy \"prod\" { })");
|
|
23
|
+
}
|
|
24
|
+
const name = this.advance().value;
|
|
25
|
+
|
|
26
|
+
this.expect(TokenType.LBRACE, "Expected '{' after deploy name");
|
|
27
|
+
const body = [];
|
|
28
|
+
|
|
29
|
+
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
|
30
|
+
try {
|
|
31
|
+
const stmt = this.parseDeployStatement();
|
|
32
|
+
if (stmt) {
|
|
33
|
+
if (Array.isArray(stmt)) {
|
|
34
|
+
body.push(...stmt);
|
|
35
|
+
} else {
|
|
36
|
+
body.push(stmt);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
} catch (e) {
|
|
40
|
+
this.errors.push(e);
|
|
41
|
+
this._synchronizeBlock();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
this.expect(TokenType.RBRACE, "Expected '}' to close deploy block");
|
|
46
|
+
return new DeployBlock(body, l, name);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
ParserClass.prototype.parseDeployStatement = function() {
|
|
50
|
+
if (this.check(TokenType.IDENTIFIER)) {
|
|
51
|
+
const val = this.current().value;
|
|
52
|
+
|
|
53
|
+
// env { KEY: "value" }
|
|
54
|
+
if (val === 'env') {
|
|
55
|
+
return this.parseDeployEnvBlock();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// db { postgres { } redis { } }
|
|
59
|
+
if (val === 'db') {
|
|
60
|
+
return this.parseDeployDbBlock();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Config field: identifier: value (e.g., domain: "myapp.com")
|
|
64
|
+
if (this.peek(1).type === TokenType.COLON && !DEPLOY_SUB_BLOCK_KEYWORDS.has(val)) {
|
|
65
|
+
return this.parseDeployConfigField();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Handle keyword tokens used as config keys (e.g., server: "root@example.com")
|
|
70
|
+
// In deploy blocks, 'server' is lexed as TokenType.SERVER, not IDENTIFIER
|
|
71
|
+
if (this.check(TokenType.SERVER) && this.peek(1).type === TokenType.COLON) {
|
|
72
|
+
return this.parseDeployConfigField();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Deploy blocks only contain config fields, env, and db sub-blocks
|
|
76
|
+
throw this.error(`Unexpected token in deploy block: "${this.current().value || this.current().type}"`);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
ParserClass.prototype.parseDeployConfigField = function() {
|
|
80
|
+
const l = this.loc();
|
|
81
|
+
const key = this.advance().value; // consume identifier (e.g., 'server')
|
|
82
|
+
this.expect(TokenType.COLON, "Expected ':' after config key");
|
|
83
|
+
const value = this.parseExpression();
|
|
84
|
+
return new DeployConfigField(key, value, l);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
ParserClass.prototype.parseDeployEnvBlock = function() {
|
|
88
|
+
const l = this.loc();
|
|
89
|
+
this.advance(); // consume 'env'
|
|
90
|
+
this.expect(TokenType.LBRACE, "Expected '{' after 'env'");
|
|
91
|
+
const entries = [];
|
|
92
|
+
|
|
93
|
+
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
|
94
|
+
const key = this.expect(TokenType.IDENTIFIER, "Expected env variable name").value;
|
|
95
|
+
this.expect(TokenType.COLON, "Expected ':' after env key");
|
|
96
|
+
const value = this.parseExpression();
|
|
97
|
+
entries.push({ key, value });
|
|
98
|
+
this.match(TokenType.COMMA);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
this.expect(TokenType.RBRACE, "Expected '}' to close env block");
|
|
102
|
+
return new DeployEnvBlock(entries, l);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
ParserClass.prototype.parseDeployDbBlock = function() {
|
|
106
|
+
const l = this.loc();
|
|
107
|
+
this.advance(); // consume 'db'
|
|
108
|
+
this.expect(TokenType.LBRACE, "Expected '{' after 'db'");
|
|
109
|
+
const blocks = [];
|
|
110
|
+
|
|
111
|
+
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
|
112
|
+
const engineLoc = this.loc();
|
|
113
|
+
const engine = this.expect(TokenType.IDENTIFIER, "Expected database engine name (e.g., postgres, redis)").value;
|
|
114
|
+
this.expect(TokenType.LBRACE, `Expected '{' after '${engine}'`);
|
|
115
|
+
const config = {};
|
|
116
|
+
|
|
117
|
+
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
|
118
|
+
const key = this.expect(TokenType.IDENTIFIER, "Expected config key").value;
|
|
119
|
+
this.expect(TokenType.COLON, "Expected ':' after config key");
|
|
120
|
+
const value = this.parseExpression();
|
|
121
|
+
config[key] = value;
|
|
122
|
+
this.match(TokenType.COMMA);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
this.expect(TokenType.RBRACE, `Expected '}' to close ${engine} config`);
|
|
126
|
+
blocks.push(new DeployDbBlock(engine, config, engineLoc));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
this.expect(TokenType.RBRACE, "Expected '}' to close db block");
|
|
130
|
+
return blocks;
|
|
131
|
+
};
|
|
132
|
+
}
|