tova 0.7.0 → 0.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/bin/tova.js +192 -10
  2. package/package.json +2 -7
  3. package/src/analyzer/analyzer.js +134 -2
  4. package/src/analyzer/deploy-analyzer.js +44 -0
  5. package/src/codegen/base-codegen.js +1159 -10
  6. package/src/codegen/codegen.js +20 -0
  7. package/src/codegen/deploy-codegen.js +49 -0
  8. package/src/codegen/shared-codegen.js +5 -0
  9. package/src/codegen/wasm-codegen.js +6 -0
  10. package/src/config/edit-toml.js +6 -2
  11. package/src/config/git-resolver.js +128 -0
  12. package/src/config/lock-file.js +57 -0
  13. package/src/config/module-cache.js +58 -0
  14. package/src/config/module-entry.js +37 -0
  15. package/src/config/module-path.js +31 -0
  16. package/src/config/pkg-errors.js +62 -0
  17. package/src/config/resolve.js +17 -0
  18. package/src/config/resolver.js +139 -0
  19. package/src/config/search.js +28 -0
  20. package/src/config/semver.js +72 -0
  21. package/src/config/toml.js +48 -5
  22. package/src/deploy/deploy.js +217 -0
  23. package/src/deploy/infer.js +218 -0
  24. package/src/deploy/provision.js +311 -0
  25. package/src/lsp/server.js +482 -0
  26. package/src/parser/ast.js +24 -0
  27. package/src/parser/concurrency-ast.js +15 -0
  28. package/src/parser/concurrency-parser.js +236 -0
  29. package/src/parser/deploy-ast.js +37 -0
  30. package/src/parser/deploy-parser.js +132 -0
  31. package/src/parser/parser.js +21 -3
  32. package/src/parser/select-ast.js +39 -0
  33. package/src/registry/plugins/concurrency-plugin.js +32 -0
  34. package/src/registry/plugins/deploy-plugin.js +33 -0
  35. package/src/registry/register-all.js +4 -0
  36. package/src/stdlib/inline.js +35 -3
  37. package/src/stdlib/runtime-bridge.js +152 -0
  38. package/src/version.js +1 -1
@@ -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
+ }
@@ -131,13 +131,15 @@ export class Parser {
131
131
  _isContextualKeyword() {
132
132
  const t = this.current().type;
133
133
  return t === TokenType.ROUTE || t === TokenType.STATE || t === TokenType.COMPUTED ||
134
- t === TokenType.EFFECT || t === TokenType.COMPONENT || t === TokenType.STORE;
134
+ t === TokenType.EFFECT || t === TokenType.COMPONENT || t === TokenType.STORE ||
135
+ t === TokenType.FORM || t === TokenType.FIELD || t === TokenType.GROUP || t === TokenType.STEPS;
135
136
  }
136
137
 
137
138
  _isContextualKeywordToken(token) {
138
139
  const t = token.type;
139
140
  return t === TokenType.ROUTE || t === TokenType.STATE || t === TokenType.COMPUTED ||
140
- t === TokenType.EFFECT || t === TokenType.COMPONENT || t === TokenType.STORE;
141
+ t === TokenType.EFFECT || t === TokenType.COMPONENT || t === TokenType.STORE ||
142
+ t === TokenType.FORM || t === TokenType.FIELD || t === TokenType.GROUP || t === TokenType.STEPS;
141
143
  }
142
144
 
143
145
  _synchronizeBlock() {
@@ -210,6 +212,13 @@ export class Parser {
210
212
  // ─── Program ───────────────────────────────────────────────
211
213
 
212
214
  parse() {
215
+ // Eagerly install all block-plugin parser extensions so they work inside function bodies
216
+ for (const plugin of BlockRegistry.all()) {
217
+ const p = plugin.parser;
218
+ if (p.install && p.installedFlag && !Parser.prototype[p.installedFlag]) {
219
+ p.install(Parser);
220
+ }
221
+ }
213
222
  const body = [];
214
223
  const maxErrors = 50; // Stop after 50 errors to avoid cascading noise
215
224
  while (!this.isAtEnd()) {
@@ -1218,7 +1227,12 @@ export class Parser {
1218
1227
  const elements = [];
1219
1228
 
1220
1229
  while (!this.check(TokenType.RBRACKET) && !this.isAtEnd()) {
1221
- if (this.check(TokenType.IDENTIFIER) && this.current().value === '_') {
1230
+ if (this.check(TokenType.SPREAD)) {
1231
+ this.advance(); // consume ...
1232
+ const restName = this.expect(TokenType.IDENTIFIER, "Expected identifier after '...'").value;
1233
+ elements.push('...' + restName);
1234
+ break; // rest must be last
1235
+ } else if (this.check(TokenType.IDENTIFIER) && this.current().value === '_') {
1222
1236
  elements.push(null); // skip placeholder
1223
1237
  this.advance();
1224
1238
  } else {
@@ -2003,6 +2017,10 @@ export class Parser {
2003
2017
  case TokenType.BROWSER:
2004
2018
  case TokenType.SHARED:
2005
2019
  case TokenType.DERIVE:
2020
+ case TokenType.FORM:
2021
+ case TokenType.FIELD:
2022
+ case TokenType.GROUP:
2023
+ case TokenType.STEPS:
2006
2024
  return new AST.Identifier(this.advance().value, l);
2007
2025
 
2008
2026
  case TokenType.IDENTIFIER: {
@@ -0,0 +1,39 @@
1
+ // Select-specific AST Node definitions for the Tova language
2
+ // Extracted for lazy loading -- only loaded when select { } blocks are used.
3
+
4
+ /**
5
+ * select {
6
+ * msg from ch => { ... }
7
+ * ch.send(val) => { ... }
8
+ * timeout(5000) => { ... }
9
+ * _ => { ... }
10
+ * }
11
+ */
12
+ export class SelectStatement {
13
+ constructor(cases, loc) {
14
+ this.type = 'SelectStatement';
15
+ this.cases = cases; // Array of SelectCase
16
+ this.loc = loc;
17
+ }
18
+ }
19
+
20
+ /**
21
+ * A single case arm inside a select block.
22
+ *
23
+ * kind: "receive" | "send" | "timeout" | "default"
24
+ * channel: Expression | null (identifier for the channel; null for timeout/default)
25
+ * binding: string | null (variable name bound on receive; null otherwise)
26
+ * value: Expression | null (value to send, or timeout duration; null for receive/default)
27
+ * body: [Statement] (statements executed when this case fires)
28
+ */
29
+ export class SelectCase {
30
+ constructor(kind, channel, binding, value, body, loc) {
31
+ this.type = 'SelectCase';
32
+ this.kind = kind; // "receive" | "send" | "timeout" | "default"
33
+ this.channel = channel; // Expression | null
34
+ this.binding = binding; // string | null
35
+ this.value = value; // Expression | null
36
+ this.body = body; // [Statement]
37
+ this.loc = loc;
38
+ }
39
+ }
@@ -0,0 +1,32 @@
1
+ import { TokenType } from '../../lexer/tokens.js';
2
+ import { installConcurrencyParser } from '../../parser/concurrency-parser.js';
3
+
4
+ export const concurrencyPlugin = {
5
+ name: 'concurrency',
6
+ astNodeType: 'ConcurrentBlock',
7
+ detection: {
8
+ strategy: 'identifier',
9
+ identifierValue: 'concurrent',
10
+ lookahead: (parser) => {
11
+ const next = parser.peek(1);
12
+ // concurrent {} or concurrent mode {}
13
+ return next.type === TokenType.LBRACE ||
14
+ (next.type === TokenType.IDENTIFIER &&
15
+ ['cancel_on_error', 'first', 'timeout'].includes(next.value));
16
+ },
17
+ },
18
+ parser: {
19
+ install: installConcurrencyParser,
20
+ installedFlag: '_concurrencyParserInstalled',
21
+ method: 'parseConcurrentBlock',
22
+ },
23
+ analyzer: {
24
+ visit: (analyzer, node) => {
25
+ if (node.type === 'SelectStatement') return analyzer.visitSelectStatement(node);
26
+ return analyzer.visitConcurrentBlock(node);
27
+ },
28
+ childNodeTypes: ['SelectStatement'],
29
+ noopNodeTypes: ['SpawnExpression', 'SelectCase'],
30
+ },
31
+ codegen: {},
32
+ };
@@ -0,0 +1,33 @@
1
+ import { installDeployParser } from '../../parser/deploy-parser.js';
2
+ import { installDeployAnalyzer } from '../../analyzer/deploy-analyzer.js';
3
+ import { TokenType } from '../../lexer/tokens.js';
4
+
5
+ export const deployPlugin = {
6
+ name: 'deploy',
7
+ astNodeType: 'DeployBlock',
8
+ detection: {
9
+ strategy: 'identifier',
10
+ identifierValue: 'deploy',
11
+ lookahead: (parser) => {
12
+ const next = parser.peek(1);
13
+ // deploy "name" {} — name is required
14
+ return next.type === TokenType.STRING;
15
+ },
16
+ },
17
+ parser: {
18
+ install: installDeployParser,
19
+ installedFlag: '_deployParserInstalled',
20
+ method: 'parseDeployBlock',
21
+ },
22
+ analyzer: {
23
+ visit: (analyzer, node) => {
24
+ installDeployAnalyzer(analyzer.constructor);
25
+ analyzer.visitDeployBlock(node);
26
+ },
27
+ childNodeTypes: [],
28
+ noopNodeTypes: [
29
+ 'DeployConfigField', 'DeployEnvBlock', 'DeployDbBlock',
30
+ ],
31
+ },
32
+ codegen: {},
33
+ };
@@ -11,6 +11,8 @@ import { dataPlugin } from './plugins/data-plugin.js';
11
11
  import { testPlugin } from './plugins/test-plugin.js';
12
12
  import { benchPlugin } from './plugins/bench-plugin.js';
13
13
  import { edgePlugin } from './plugins/edge-plugin.js';
14
+ import { concurrencyPlugin } from './plugins/concurrency-plugin.js';
15
+ import { deployPlugin } from './plugins/deploy-plugin.js';
14
16
 
15
17
  BlockRegistry.register(serverPlugin);
16
18
  BlockRegistry.register(browserPlugin);
@@ -21,5 +23,7 @@ BlockRegistry.register(dataPlugin);
21
23
  BlockRegistry.register(testPlugin);
22
24
  BlockRegistry.register(benchPlugin);
23
25
  BlockRegistry.register(edgePlugin);
26
+ BlockRegistry.register(concurrencyPlugin);
27
+ BlockRegistry.register(deployPlugin);
24
28
 
25
29
  export { BlockRegistry };
@@ -117,7 +117,7 @@ export const BUILTIN_FUNCTIONS = {
117
117
  if (!parallel_map._pool) {
118
118
  const { Worker } = await import("worker_threads");
119
119
  const wc = 'const{parentPort}=require("worker_threads");parentPort.on("message",m=>{const fn=(0,eval)("("+m.f+")");try{const r=m.c.map(fn);parentPort.postMessage({i:m.i,r})}catch(e){parentPort.postMessage({i:m.i,e:e.message})}})';
120
- parallel_map._pool = Array.from({length: n}, () => { const w = new Worker(wc, {eval: true}); w.unref(); return w; });
120
+ parallel_map._pool = Array.from({length: n}, () => new Worker(wc, {eval: true}));
121
121
  parallel_map._cid = 0;
122
122
  }
123
123
  const pool = parallel_map._pool;
@@ -125,17 +125,20 @@ export const BUILTIN_FUNCTIONS = {
125
125
  const fnStr = fn.toString();
126
126
  const cid = ++parallel_map._cid;
127
127
  const promises = [];
128
+ const usedWorkers = [];
128
129
  for (let ci = 0; ci < pool.length && ci * cs < arr.length; ci++) {
129
130
  const chunk = arr.slice(ci * cs, (ci + 1) * cs);
130
131
  const mid = cid * 1000 + ci;
132
+ const w = pool[ci];
133
+ w.ref();
134
+ usedWorkers.push(w);
131
135
  promises.push(new Promise((resolve, reject) => {
132
- const w = pool[ci];
133
136
  const h = (msg) => { if (msg.i === mid) { w.removeListener("message", h); if (msg.e) reject(new Error(msg.e)); else resolve(msg.r); } };
134
137
  w.on("message", h);
135
138
  w.postMessage({i: mid, c: chunk, f: fnStr});
136
139
  }));
137
140
  }
138
- return (await Promise.all(promises)).flat();
141
+ try { return (await Promise.all(promises)).flat(); } finally { for (const w of usedWorkers) w.unref(); }
139
142
  }`,
140
143
  upper: `function upper(s) { return s.toUpperCase(); }`,
141
144
  lower: `function lower(s) { return s.toLowerCase(); }`,
@@ -1054,6 +1057,35 @@ Table.prototype = { get rows() { return this._rows.length; }, get columns() { re
1054
1057
  this._recvWaiters.push(resolve);
1055
1058
  }.bind(this));
1056
1059
  }
1060
+ _tryReceive() {
1061
+ if (this._buffer.length > 0) {
1062
+ const value = this._buffer.shift();
1063
+ if (this._sendWaiters.length > 0) {
1064
+ const waiter = this._sendWaiters.shift();
1065
+ this._buffer.push(waiter.value);
1066
+ waiter.resolve();
1067
+ }
1068
+ return Some(value);
1069
+ }
1070
+ if (this._sendWaiters.length > 0) {
1071
+ const waiter = this._sendWaiters.shift();
1072
+ waiter.resolve();
1073
+ return Some(waiter.value);
1074
+ }
1075
+ return None;
1076
+ }
1077
+ _trySend(value) {
1078
+ if (this._recvWaiters.length > 0) {
1079
+ const waiter = this._recvWaiters.shift();
1080
+ waiter(Some(value));
1081
+ return true;
1082
+ }
1083
+ if (this._capacity > 0 && this._buffer.length < this._capacity) {
1084
+ this._buffer.push(value);
1085
+ return true;
1086
+ }
1087
+ return false;
1088
+ }
1057
1089
  close() {
1058
1090
  this._closed = true;
1059
1091
  for (const waiter of this._recvWaiters) waiter(None);