future-lang 0.3.0
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/ARCHITECTURE.md +424 -0
- package/MIGRATION.md +365 -0
- package/README.md +370 -0
- package/ROADMAP.md +263 -0
- package/examples/adult.future +8 -0
- package/examples/api.future +11 -0
- package/examples/assistant.future +8 -0
- package/examples/browser-demo.html +164 -0
- package/examples/greet.future +7 -0
- package/examples/hello.future +1 -0
- package/examples/math.future +8 -0
- package/examples/mini-app.html +301 -0
- package/examples/smarthome.future +10 -0
- package/future-browser.js +102 -0
- package/future-playground.html +650 -0
- package/package.json +27 -0
- package/runtime/ai.js +92 -0
- package/runtime/browser.js +458 -0
- package/runtime/device.js +36 -0
- package/runtime/home.js +19 -0
- package/runtime/http.js +32 -0
- package/runtime/index.js +403 -0
- package/runtime/lsp-metadata.js +104 -0
- package/runtime/math.js +16 -0
- package/runtime/memory.js +61 -0
- package/runtime/mqtt.js +49 -0
- package/runtime/providers/anthropic.js +59 -0
- package/runtime/providers/index.js +93 -0
- package/runtime/providers/openai-compat.js +85 -0
- package/runtime/providers/util.js +70 -0
- package/runtime/rag/chunker.js +65 -0
- package/runtime/rag/pipeline.js +86 -0
- package/runtime/rag/vector-store.js +119 -0
- package/runtime/rag.js +94 -0
- package/runtime/schedule.js +77 -0
- package/runtime/system.js +101 -0
- package/runtime/tts.js +38 -0
- package/runtime/vision.js +85 -0
- package/server.js +42 -0
- package/src/ast.js +202 -0
- package/src/cli.js +391 -0
- package/src/errors.js +21 -0
- package/src/formatter.js +48 -0
- package/src/generator.js +457 -0
- package/src/index.js +48 -0
- package/src/lexer.js +248 -0
- package/src/parser.js +469 -0
package/src/generator.js
ADDED
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
// generator.js
|
|
2
|
+
// Phase 3 — JavaScript code generation.
|
|
3
|
+
//
|
|
4
|
+
// Two modes:
|
|
5
|
+
// * SIMPLE — pure programs (print/vars/if/functions). Emits plain, sync JS,
|
|
6
|
+
// exactly like before.
|
|
7
|
+
// * ASYNC — the program uses a capability (http/ai/mqtt/tts/...). Emits an
|
|
8
|
+
// ES module that imports the runtime, makes every function async
|
|
9
|
+
// and `await`s every call. The user never writes `await` — the
|
|
10
|
+
// language stays human-readable while the JS stays correct.
|
|
11
|
+
//
|
|
12
|
+
// Capabilities are recognised purely by their namespace (the object of a member
|
|
13
|
+
// call). Adding rag/vision/home — or anything else — is just a name in this set
|
|
14
|
+
// plus a matching runtime module. No other compiler change required.
|
|
15
|
+
|
|
16
|
+
import { NodeType } from './ast.js';
|
|
17
|
+
import { FutureError } from './errors.js';
|
|
18
|
+
|
|
19
|
+
const INDENT = ' ';
|
|
20
|
+
|
|
21
|
+
/** Namespaces that resolve to runtime modules. Extend freely. */
|
|
22
|
+
export const NAMESPACES = new Set([
|
|
23
|
+
'ai', 'http', 'mqtt', 'tts', // core modules
|
|
24
|
+
'rag', 'vision', 'home', // AI / automation extension points
|
|
25
|
+
'memory', 'schedule', 'system', 'device', // optional new modules
|
|
26
|
+
'math', // general-purpose math
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
export class Generator {
|
|
30
|
+
/**
|
|
31
|
+
* @param {object} program
|
|
32
|
+
* @param {{ runtimeSpecifier?: string }} [options]
|
|
33
|
+
* @returns {string} JavaScript source.
|
|
34
|
+
*/
|
|
35
|
+
generate(program, options = {}) {
|
|
36
|
+
this.runtimeSpecifier = options.runtimeSpecifier ?? 'future-lang/runtime';
|
|
37
|
+
this.browserMode = options.browserMode ?? false;
|
|
38
|
+
this.isModule = options.isModule ?? false;
|
|
39
|
+
// Map<importedFuturePath, string[]> — exported names for non-aliased use statements.
|
|
40
|
+
this.importedNames = options.importedNames ?? new Map();
|
|
41
|
+
// Map<importedFuturePath, resolvedJsPath> — path override for `future run` temp files.
|
|
42
|
+
this.pathMap = options.pathMap ?? new Map();
|
|
43
|
+
// Aliases declared by `use "..." as alias` — must NOT be routed through __rt.
|
|
44
|
+
this.useAliases = new Set(
|
|
45
|
+
program.body
|
|
46
|
+
.filter((s) => s.type === NodeType.UseStatement && s.alias)
|
|
47
|
+
.map((s) => s.alias),
|
|
48
|
+
);
|
|
49
|
+
this.asyncMode = usesRuntime(program, this.useAliases);
|
|
50
|
+
|
|
51
|
+
const lines = [];
|
|
52
|
+
|
|
53
|
+
// Emit use/import statements first (always, regardless of asyncMode).
|
|
54
|
+
const useStmts = program.body.filter((s) => s.type === NodeType.UseStatement);
|
|
55
|
+
for (const stmt of useStmts) {
|
|
56
|
+
lines.push(this.genUseStatement(stmt));
|
|
57
|
+
}
|
|
58
|
+
if (useStmts.length > 0) lines.push('');
|
|
59
|
+
|
|
60
|
+
if (this.asyncMode && !this.browserMode) {
|
|
61
|
+
lines.push(`import { runtime as __rt } from ${JSON.stringify(this.runtimeSpecifier)};`);
|
|
62
|
+
lines.push('');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Emit __len helper only when len() is actually used — keeps simple programs clean.
|
|
66
|
+
if (usesBuiltin(program, 'len')) {
|
|
67
|
+
lines.push('function __len(x) { return x == null ? 0 : (x.length ?? Object.keys(x).length); }');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const declarations = collectAssignedNames(program.body);
|
|
71
|
+
if (declarations.length > 0) {
|
|
72
|
+
lines.push(`let ${declarations.join(', ')};`);
|
|
73
|
+
}
|
|
74
|
+
for (const stmt of program.body) {
|
|
75
|
+
if (stmt.type === NodeType.UseStatement) continue; // already emitted above
|
|
76
|
+
lines.push(this.genStatement(stmt, 0, /* topLevel= */ true));
|
|
77
|
+
}
|
|
78
|
+
return lines.join('\n') + '\n';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Emit an ES `import` for a `use` statement. */
|
|
82
|
+
genUseStatement(node) {
|
|
83
|
+
const jsPath = node.path.replace(/\.future$/, '.js');
|
|
84
|
+
const resolved = this.pathMap.get(node.path) ?? jsPath;
|
|
85
|
+
if (node.alias) {
|
|
86
|
+
return `import * as ${node.alias} from ${JSON.stringify(resolved)};`;
|
|
87
|
+
}
|
|
88
|
+
const names = this.importedNames.get(node.path) ?? [];
|
|
89
|
+
if (names.length > 0) {
|
|
90
|
+
return `import { ${names.join(', ')} } from ${JSON.stringify(resolved)};`;
|
|
91
|
+
}
|
|
92
|
+
// Fallback: wildcard namespace (when imported file couldn't be analysed).
|
|
93
|
+
const id = node.path.replace(/[^a-zA-Z0-9]/g, '_');
|
|
94
|
+
return `import * as __mod${id} from ${JSON.stringify(resolved)};`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
genStatement(node, depth, topLevel = false) {
|
|
98
|
+
const pad = INDENT.repeat(depth);
|
|
99
|
+
switch (node.type) {
|
|
100
|
+
case NodeType.PrintStatement:
|
|
101
|
+
// In browser mode, route through __rt.print so the caller can redirect output.
|
|
102
|
+
return this.browserMode
|
|
103
|
+
? `${pad}__rt.print(${this.genExpression(node.expression)});`
|
|
104
|
+
: `${pad}console.log(${this.genExpression(node.expression)});`;
|
|
105
|
+
|
|
106
|
+
case NodeType.Assignment:
|
|
107
|
+
return `${pad}${node.name} = ${this.genExpression(node.value)};`;
|
|
108
|
+
|
|
109
|
+
case NodeType.IfStatement: {
|
|
110
|
+
let out = `${pad}if (${this.genExpression(node.condition)}) {\n`;
|
|
111
|
+
out += this.genBody(node.consequent, depth + 1);
|
|
112
|
+
out += `\n${pad}}`;
|
|
113
|
+
if (node.alternate) {
|
|
114
|
+
out += ` else {\n${this.genBody(node.alternate, depth + 1)}\n${pad}}`;
|
|
115
|
+
}
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
case NodeType.FunctionDeclaration: {
|
|
120
|
+
const exportKw = (this.isModule && topLevel) ? 'export ' : '';
|
|
121
|
+
const kw = this.asyncMode ? 'async function' : 'function';
|
|
122
|
+
const params = node.params.join(', ');
|
|
123
|
+
const locals = collectAssignedNames(node.body)
|
|
124
|
+
.filter((name) => !node.params.includes(name));
|
|
125
|
+
const inner = [];
|
|
126
|
+
if (locals.length > 0) {
|
|
127
|
+
inner.push(`${INDENT.repeat(depth + 1)}let ${locals.join(', ')};`);
|
|
128
|
+
}
|
|
129
|
+
for (const stmt of node.body) inner.push(this.genStatement(stmt, depth + 1));
|
|
130
|
+
return `${pad}${exportKw}${kw} ${node.name}(${params}) {\n${inner.join('\n')}\n${pad}}`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
case NodeType.ReturnStatement:
|
|
134
|
+
return node.argument
|
|
135
|
+
? `${pad}return ${this.genExpression(node.argument)};`
|
|
136
|
+
: `${pad}return;`;
|
|
137
|
+
|
|
138
|
+
case NodeType.ExpressionStatement:
|
|
139
|
+
return `${pad}${this.genExpression(node.expression)};`;
|
|
140
|
+
|
|
141
|
+
case NodeType.ForStatement: {
|
|
142
|
+
// `for item in list ... end` → `for (const item of list) { ... }`
|
|
143
|
+
const iter = this.genExpression(node.iterable);
|
|
144
|
+
const inner = this.genBody(node.body, depth + 1);
|
|
145
|
+
return `${pad}for (const ${node.variable} of ${iter}) {\n${inner}\n${pad}}`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
case NodeType.WhileStatement: {
|
|
149
|
+
// `while cond ... end` → `while (cond) { ... }`
|
|
150
|
+
const cond = this.genExpression(node.condition);
|
|
151
|
+
const inner = this.genBody(node.body, depth + 1);
|
|
152
|
+
return `${pad}while (${cond}) {\n${inner}\n${pad}}`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
case NodeType.StreamStatement: {
|
|
156
|
+
// `stream ai.ask("prompt") ... end`
|
|
157
|
+
// → `await __rt.ai.stream("prompt", async (chunk) => { ... })`
|
|
158
|
+
// The call must be a namespace capability call.
|
|
159
|
+
const call = node.call;
|
|
160
|
+
if (
|
|
161
|
+
call.type !== NodeType.CallExpression ||
|
|
162
|
+
call.callee.type !== NodeType.MemberExpression ||
|
|
163
|
+
call.callee.object.type !== NodeType.Identifier ||
|
|
164
|
+
!NAMESPACES.has(call.callee.object.name)
|
|
165
|
+
) {
|
|
166
|
+
throw new FutureError(
|
|
167
|
+
'stream requires a capability call, e.g. stream ai.ask("prompt")',
|
|
168
|
+
node.line, node.column, 'codegen',
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
const ns = call.callee.object.name;
|
|
172
|
+
const args = call.arguments.map((a) => this.genExpression(a)).join(', ');
|
|
173
|
+
const sep = args ? ', ' : '';
|
|
174
|
+
const inner = this.genBody(node.body, depth + 1);
|
|
175
|
+
return `${pad}await __rt.${ns}.stream(${args}${sep}async (chunk) => {\n${inner}\n${pad}});`;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
case NodeType.TryStatement: {
|
|
179
|
+
// `try ... catch err ... end` → `try { ... } catch (err) { ... }`
|
|
180
|
+
const tryBody = this.genBody(node.body, depth + 1);
|
|
181
|
+
const catchBody = this.genBody(node.catchBody, depth + 1);
|
|
182
|
+
return (
|
|
183
|
+
`${pad}try {\n${tryBody}\n${pad}} catch (${node.catchVar}) {\n${catchBody}\n${pad}}`
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
case NodeType.AgentDeclaration: {
|
|
188
|
+
// `agent name use cap ... end`
|
|
189
|
+
// Compiles to: async function name(goal) { let locals; body }
|
|
190
|
+
// `use` declarations are no-ops in generated code — they exist for tooling.
|
|
191
|
+
// `goal` is the implicit parameter; filter it from hoisted locals.
|
|
192
|
+
const locals = collectAssignedNames(node.body)
|
|
193
|
+
.filter((n) => n !== 'goal');
|
|
194
|
+
const inner = [];
|
|
195
|
+
if (locals.length > 0) {
|
|
196
|
+
inner.push(`${INDENT.repeat(depth + 1)}let ${locals.join(', ')};`);
|
|
197
|
+
}
|
|
198
|
+
for (const stmt of node.body) inner.push(this.genStatement(stmt, depth + 1));
|
|
199
|
+
return `${pad}async function ${node.name}(goal) {\n${inner.join('\n')}\n${pad}}`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
case NodeType.OnStatement: {
|
|
203
|
+
// `on mqtt "topic" ... end`
|
|
204
|
+
// Compiles to: await __rt.<source>.subscribe(<channel>, async (message) => { ... })
|
|
205
|
+
const inner = this.genBody(node.body, depth + 1);
|
|
206
|
+
const chan = this.genExpression(node.channel);
|
|
207
|
+
return `${pad}await __rt.${node.source}.subscribe(${chan}, async (message) => {\n${inner}\n${pad}});`;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
case NodeType.EveryStatement: {
|
|
211
|
+
// `every "30m" ... end`
|
|
212
|
+
// Compiles to: await __rt.schedule.every(<interval>, async () => { ... })
|
|
213
|
+
const inner = this.genBody(node.body, depth + 1);
|
|
214
|
+
const interval = this.genExpression(node.interval);
|
|
215
|
+
return `${pad}await __rt.schedule.every(${interval}, async () => {\n${inner}\n${pad}});`;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
default:
|
|
219
|
+
throw new FutureError(
|
|
220
|
+
`Cannot generate statement of type ${node.type}`,
|
|
221
|
+
node.line, node.column, 'codegen',
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
genBody(statements, depth) {
|
|
227
|
+
return statements.map((s) => this.genStatement(s, depth)).join('\n');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
genExpression(node) {
|
|
231
|
+
switch (node.type) {
|
|
232
|
+
case NodeType.NumberLiteral:
|
|
233
|
+
return String(node.value);
|
|
234
|
+
case NodeType.StringLiteral:
|
|
235
|
+
return this.genStringLiteral(node.value);
|
|
236
|
+
case NodeType.BooleanLiteral:
|
|
237
|
+
return node.value ? 'true' : 'false';
|
|
238
|
+
case NodeType.NullLiteral:
|
|
239
|
+
return 'null';
|
|
240
|
+
case NodeType.Identifier:
|
|
241
|
+
return node.name;
|
|
242
|
+
case NodeType.MemberExpression: {
|
|
243
|
+
const obj = node.object;
|
|
244
|
+
// In async mode, direct member access on a runtime namespace routes through __rt.
|
|
245
|
+
// Aliases from `use "..." as alias` are NOT runtime namespaces.
|
|
246
|
+
if (
|
|
247
|
+
this.asyncMode &&
|
|
248
|
+
obj.type === NodeType.Identifier &&
|
|
249
|
+
NAMESPACES.has(obj.name) &&
|
|
250
|
+
!this.useAliases.has(obj.name)
|
|
251
|
+
) {
|
|
252
|
+
return `__rt.${obj.name}.${node.property}`;
|
|
253
|
+
}
|
|
254
|
+
return `${this.genExpression(obj)}.${node.property}`;
|
|
255
|
+
}
|
|
256
|
+
case NodeType.CallExpression:
|
|
257
|
+
return this.genCall(node);
|
|
258
|
+
case NodeType.UnaryExpression: {
|
|
259
|
+
const op = node.operator === 'not' ? '!' : '-';
|
|
260
|
+
return `${op}${this.genExpression(node.argument)}`;
|
|
261
|
+
}
|
|
262
|
+
case NodeType.BinaryExpression: {
|
|
263
|
+
const op = mapOperator(node.operator);
|
|
264
|
+
return `(${this.genExpression(node.left)} ${op} ${this.genExpression(node.right)})`;
|
|
265
|
+
}
|
|
266
|
+
case NodeType.ArrayLiteral: {
|
|
267
|
+
const els = node.elements.map((e) => this.genExpression(e)).join(', ');
|
|
268
|
+
return `[${els}]`;
|
|
269
|
+
}
|
|
270
|
+
case NodeType.ObjectLiteral: {
|
|
271
|
+
if (node.properties.length === 0) return '{}';
|
|
272
|
+
const props = node.properties
|
|
273
|
+
.map((p) => `${JSON.stringify(p.key)}: ${this.genExpression(p.value)}`)
|
|
274
|
+
.join(', ');
|
|
275
|
+
return `{ ${props} }`;
|
|
276
|
+
}
|
|
277
|
+
default:
|
|
278
|
+
throw new FutureError(
|
|
279
|
+
`Cannot generate expression of type ${node.type}`,
|
|
280
|
+
node.line, node.column, 'codegen',
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
genCall(node) {
|
|
286
|
+
const args = node.arguments.map((a) => this.genExpression(a)).join(', ');
|
|
287
|
+
const callee = node.callee;
|
|
288
|
+
|
|
289
|
+
// Capability call, e.g. http.get(...) -> await __rt.http.get(...)
|
|
290
|
+
if (isNamespaceCall(node, this.useAliases)) {
|
|
291
|
+
return `await __rt.${callee.object.name}.${callee.property}(${args})`;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (callee.type === NodeType.Identifier) {
|
|
295
|
+
// len(x) → __len(x) (sync built-in, no runtime dependency)
|
|
296
|
+
if (callee.name === 'len') return `__len(${args})`;
|
|
297
|
+
// input(prompt) → await __rt.input(prompt) (async built-in)
|
|
298
|
+
if (callee.name === 'input') return `await __rt.input(${args})`;
|
|
299
|
+
// User function call. In async mode every function is async, so await it.
|
|
300
|
+
return this.asyncMode ? `await ${callee.name}(${args})` : `${callee.name}(${args})`;
|
|
301
|
+
}
|
|
302
|
+
// Method call on a runtime-returned object (e.g. kb.query(), kb.index()).
|
|
303
|
+
// In async mode we await ALL method calls: `await x` on a sync value is harmless,
|
|
304
|
+
// and it ensures calls like kb.query() — where kb holds an async-returning object —
|
|
305
|
+
// are properly awaited. This is required for the Knowledge Base API to work correctly.
|
|
306
|
+
if (this.asyncMode && callee.type === NodeType.MemberExpression) {
|
|
307
|
+
return `await ${this.genExpression(callee)}(${args})`;
|
|
308
|
+
}
|
|
309
|
+
return `${this.genExpression(callee)}(${args})`;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Generate a JS string literal from a Future string value.
|
|
314
|
+
* If the string contains `{identifier}` patterns, emits a JS template literal.
|
|
315
|
+
* `\{` in the source was stored as \x01 by the lexer — we restore it as a literal `{`.
|
|
316
|
+
* In async mode, `{namespace.prop}` is rewritten to `${__rt.namespace.prop}`.
|
|
317
|
+
*/
|
|
318
|
+
genStringLiteral(value) {
|
|
319
|
+
const withEscapes = value.replace(/\x01/g, '{');
|
|
320
|
+
const INTERP = /\{[a-zA-Z_][a-zA-Z0-9_.]*\}/;
|
|
321
|
+
if (!INTERP.test(withEscapes)) {
|
|
322
|
+
return JSON.stringify(withEscapes);
|
|
323
|
+
}
|
|
324
|
+
const safe = withEscapes
|
|
325
|
+
.replace(/\\/g, '\\\\')
|
|
326
|
+
.replace(/`/g, '\\`')
|
|
327
|
+
.replace(/\$\{/g, '\\${');
|
|
328
|
+
const templated = safe.replace(/\{([a-zA-Z_][a-zA-Z0-9_.]*)\}/g, (_, expr) => {
|
|
329
|
+
if (this.asyncMode) {
|
|
330
|
+
const dot = expr.indexOf('.');
|
|
331
|
+
const first = dot === -1 ? expr : expr.slice(0, dot);
|
|
332
|
+
if (NAMESPACES.has(first)) return `\${__rt.${expr}}`;
|
|
333
|
+
}
|
|
334
|
+
return `\${${expr}}`;
|
|
335
|
+
});
|
|
336
|
+
return `\`${templated}\``;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* True if a CallExpression's callee is `<namespace>.<method>` where the namespace
|
|
342
|
+
* is a runtime module (not a user-defined `use ... as` alias).
|
|
343
|
+
*/
|
|
344
|
+
function isNamespaceCall(node, useAliases = new Set()) {
|
|
345
|
+
const c = node.callee;
|
|
346
|
+
return (
|
|
347
|
+
node.type === NodeType.CallExpression &&
|
|
348
|
+
c.type === NodeType.MemberExpression &&
|
|
349
|
+
c.object.type === NodeType.Identifier &&
|
|
350
|
+
NAMESPACES.has(c.object.name) &&
|
|
351
|
+
!useAliases.has(c.object.name)
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** Walk the whole AST and report whether any capability call is present. */
|
|
356
|
+
function usesRuntime(node, useAliases = new Set()) {
|
|
357
|
+
if (!node || typeof node !== 'object') return false;
|
|
358
|
+
if (node.type === NodeType.CallExpression && isNamespaceCall(node, useAliases)) return true;
|
|
359
|
+
// input() is a built-in async function that needs __rt.
|
|
360
|
+
if (node.type === NodeType.CallExpression &&
|
|
361
|
+
node.callee.type === NodeType.Identifier &&
|
|
362
|
+
node.callee.name === 'input') return true;
|
|
363
|
+
// Direct namespace property access (e.g. math.pi, math.e) also needs __rt.
|
|
364
|
+
if (node.type === NodeType.MemberExpression &&
|
|
365
|
+
node.object?.type === NodeType.Identifier &&
|
|
366
|
+
NAMESPACES.has(node.object.name) &&
|
|
367
|
+
!useAliases.has(node.object.name)) return true;
|
|
368
|
+
// String interpolation referencing a namespace, e.g. "π = {math.pi}".
|
|
369
|
+
// The {namespace.prop} pattern lives inside the string value, not as an AST node.
|
|
370
|
+
if (node.type === NodeType.StringLiteral) {
|
|
371
|
+
const RE = /\{([a-zA-Z_][a-zA-Z0-9]*)\.[a-zA-Z0-9_.]+\}/g;
|
|
372
|
+
for (const m of String(node.value).matchAll(RE)) {
|
|
373
|
+
if (NAMESPACES.has(m[1])) return true;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
// Event-oriented, streaming, and agent statements always require async mode.
|
|
377
|
+
if (node.type === NodeType.OnStatement || node.type === NodeType.EveryStatement) return true;
|
|
378
|
+
if (node.type === NodeType.StreamStatement) return true;
|
|
379
|
+
// AgentDeclaration always compiles to an async function — callers must await it.
|
|
380
|
+
if (node.type === NodeType.AgentDeclaration) return true;
|
|
381
|
+
// For, While, and Try: walk their bodies (handled by the generic key loop below).
|
|
382
|
+
for (const key of Object.keys(node)) {
|
|
383
|
+
const value = node[key];
|
|
384
|
+
if (Array.isArray(value)) {
|
|
385
|
+
if (value.some((v) => usesRuntime(v, useAliases))) return true;
|
|
386
|
+
} else if (value && typeof value === 'object' && value.type) {
|
|
387
|
+
if (usesRuntime(value, useAliases)) return true;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return false;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/** Walk the AST and check if a specific built-in function name is called. */
|
|
394
|
+
function usesBuiltin(node, name) {
|
|
395
|
+
if (!node || typeof node !== 'object') return false;
|
|
396
|
+
if (node.type === NodeType.CallExpression &&
|
|
397
|
+
node.callee.type === NodeType.Identifier &&
|
|
398
|
+
node.callee.name === name) return true;
|
|
399
|
+
for (const key of Object.keys(node)) {
|
|
400
|
+
const v = node[key];
|
|
401
|
+
if (Array.isArray(v)) { if (v.some((c) => usesBuiltin(c, name))) return true; }
|
|
402
|
+
else if (v && typeof v === 'object' && v.type) { if (usesBuiltin(v, name)) return true; }
|
|
403
|
+
}
|
|
404
|
+
return false;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function mapOperator(op) {
|
|
408
|
+
switch (op) {
|
|
409
|
+
case '==': return '===';
|
|
410
|
+
case '!=': return '!==';
|
|
411
|
+
case 'and': return '&&';
|
|
412
|
+
case 'or': return '||';
|
|
413
|
+
default: return op;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Names assigned at one scope level — hoisted to a `let` declaration.
|
|
419
|
+
* Recurses into if/for/try bodies (all share the same JS scope).
|
|
420
|
+
* Does NOT recurse into FunctionDeclaration, OnStatement, EveryStatement
|
|
421
|
+
* (those create new JS function scopes).
|
|
422
|
+
*/
|
|
423
|
+
function collectAssignedNames(statements) {
|
|
424
|
+
const names = new Set();
|
|
425
|
+
const visit = (stmts) => {
|
|
426
|
+
for (const node of stmts) {
|
|
427
|
+
if (node.type === NodeType.Assignment) {
|
|
428
|
+
names.add(node.name);
|
|
429
|
+
} else if (node.type === NodeType.IfStatement) {
|
|
430
|
+
visit(node.consequent);
|
|
431
|
+
if (node.alternate) visit(node.alternate);
|
|
432
|
+
} else if (node.type === NodeType.ForStatement) {
|
|
433
|
+
// Loop variable is declared `const` in the for-of — do NOT hoist it.
|
|
434
|
+
// But assignments inside the body DO belong to the outer scope.
|
|
435
|
+
visit(node.body);
|
|
436
|
+
} else if (node.type === NodeType.WhileStatement) {
|
|
437
|
+
visit(node.body);
|
|
438
|
+
} else if (node.type === NodeType.TryStatement) {
|
|
439
|
+
// Catch variable is declared in catch() — do NOT hoist it.
|
|
440
|
+
// But other assignments in try/catch bodies belong to the outer scope.
|
|
441
|
+
visit(node.body);
|
|
442
|
+
visit(node.catchBody);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
visit(statements);
|
|
447
|
+
return [...names];
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* @param {object} program
|
|
452
|
+
* @param {{ runtimeSpecifier?: string }} [options]
|
|
453
|
+
* @returns {string}
|
|
454
|
+
*/
|
|
455
|
+
export function generate(program, options = {}) {
|
|
456
|
+
return new Generator().generate(program, options);
|
|
457
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// index.js
|
|
2
|
+
// Public library API: ties the three phases together.
|
|
3
|
+
|
|
4
|
+
import { tokenize } from './lexer.js';
|
|
5
|
+
import { parse } from './parser.js';
|
|
6
|
+
import { generate } from './generator.js';
|
|
7
|
+
|
|
8
|
+
export { tokenize } from './lexer.js';
|
|
9
|
+
export { parse } from './parser.js';
|
|
10
|
+
export { generate, NAMESPACES } from './generator.js';
|
|
11
|
+
export { FutureError } from './errors.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Compile Future source into JavaScript source.
|
|
15
|
+
* @param {string} source
|
|
16
|
+
* @param {object} [options]
|
|
17
|
+
* @param {string} [options.runtimeSpecifier] Import path for the capability runtime.
|
|
18
|
+
* @param {boolean} [options.isModule] Emit `export` for top-level functions.
|
|
19
|
+
* @param {Function} [options.resolveSource] (path: string) => string | null
|
|
20
|
+
* Called for each non-aliased `use` to extract exported names.
|
|
21
|
+
* @param {Map} [options.pathMap] Override resolved JS paths (for `run`).
|
|
22
|
+
* @returns {string} JavaScript source.
|
|
23
|
+
*/
|
|
24
|
+
export function compile(source, options = {}) {
|
|
25
|
+
const tokens = tokenize(source);
|
|
26
|
+
const ast = parse(tokens);
|
|
27
|
+
|
|
28
|
+
// For non-aliased `use` statements, read the imported file and extract function names
|
|
29
|
+
// so we can emit named imports instead of wildcard imports.
|
|
30
|
+
const importedNames = new Map();
|
|
31
|
+
if (options.resolveSource) {
|
|
32
|
+
for (const stmt of ast.body) {
|
|
33
|
+
if (stmt.type !== 'UseStatement' || stmt.alias) continue;
|
|
34
|
+
try {
|
|
35
|
+
const importedSrc = options.resolveSource(stmt.path);
|
|
36
|
+
if (importedSrc) {
|
|
37
|
+
const importedAst = parse(tokenize(importedSrc));
|
|
38
|
+
const names = importedAst.body
|
|
39
|
+
.filter((s) => s.type === 'FunctionDeclaration')
|
|
40
|
+
.map((s) => s.name);
|
|
41
|
+
if (names.length > 0) importedNames.set(stmt.path, names);
|
|
42
|
+
}
|
|
43
|
+
} catch { /* missing file — fall back to wildcard import */ }
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return generate(ast, { ...options, importedNames });
|
|
48
|
+
}
|