future-lang 0.3.1 → 0.4.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/FUTURE_FOR_LLMS.md +331 -0
- package/README.md +46 -5
- package/package.json +1 -1
- package/runtime/index.js +32 -2
- package/src/cli.js +12 -2
- package/src/generator.js +42 -7
- package/src/index.js +2 -0
- package/src/parser.js +55 -24
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
# Future — Reference for LLMs
|
|
2
|
+
|
|
3
|
+
Future is a small language that compiles to JavaScript. It does NOT exist in your training data. Read this file before generating Future code.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Reserved words
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
print if else end function return true false null none
|
|
11
|
+
and or not for in while try catch on every stream
|
|
12
|
+
agent use as
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Reserved namespaces (cannot be reassigned or used as function names):
|
|
16
|
+
```
|
|
17
|
+
ai http mqtt tts rag vision home memory schedule system device math
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Grammar (simplified BNF)
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
program = statement*
|
|
26
|
+
statement = print | assignment | if | function | return
|
|
27
|
+
| for | while | try | on | every | stream | agent | use | expr_stmt
|
|
28
|
+
|
|
29
|
+
print = "print" expression
|
|
30
|
+
assignment = IDENTIFIER "=" expression
|
|
31
|
+
if = "if" expression block ("else if" expression block)* ("else" block)? "end"
|
|
32
|
+
function = "function" IDENTIFIER "(" params ")" block "end"
|
|
33
|
+
return = "return" expression?
|
|
34
|
+
for = "for" IDENTIFIER "in" expression block "end"
|
|
35
|
+
while = "while" expression block "end"
|
|
36
|
+
try = "try" block "catch" IDENTIFIER block "end"
|
|
37
|
+
on = "on" IDENTIFIER expression block "end"
|
|
38
|
+
every = "every" expression block "end"
|
|
39
|
+
stream = "stream" call_expr block "end"
|
|
40
|
+
agent = "agent" IDENTIFIER ("use" IDENTIFIER)* block "end"
|
|
41
|
+
use = "use" STRING ("as" IDENTIFIER)?
|
|
42
|
+
|
|
43
|
+
block = statement*
|
|
44
|
+
params = (IDENTIFIER ("," IDENTIFIER)*)?
|
|
45
|
+
expression = or_expr
|
|
46
|
+
call_expr = IDENTIFIER "(" args ")"
|
|
47
|
+
| IDENTIFIER "." IDENTIFIER "(" args ")"
|
|
48
|
+
args = (expression ("," expression)*)?
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Syntax rules
|
|
54
|
+
|
|
55
|
+
- Blocks end with `end` — NO curly braces, NO semicolons
|
|
56
|
+
- `#` starts a line comment
|
|
57
|
+
- Strings: `"double"` or `'single'`
|
|
58
|
+
- String interpolation: `"Hello, {name}!"` — any `{identifier}` or `{identifier.prop}`
|
|
59
|
+
- Escape literal brace: `\{`
|
|
60
|
+
- `null` and `none` are the same
|
|
61
|
+
- Commas in lists: required — `[1, 2, 3]`
|
|
62
|
+
- Commas in objects: optional — `{ name: "Alice" age: 30 }` or `{ name: "Alice", age: 30 }`
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Every construct with example
|
|
67
|
+
|
|
68
|
+
### Variables
|
|
69
|
+
```
|
|
70
|
+
name = "Alice"
|
|
71
|
+
age = 30
|
|
72
|
+
ok = true
|
|
73
|
+
data = null
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Print
|
|
77
|
+
```
|
|
78
|
+
print "Hello, {name}!"
|
|
79
|
+
print age
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### If / else if / else
|
|
83
|
+
```
|
|
84
|
+
if score >= 90
|
|
85
|
+
print "A"
|
|
86
|
+
else if score >= 80
|
|
87
|
+
print "B"
|
|
88
|
+
else if score >= 70
|
|
89
|
+
print "C"
|
|
90
|
+
else
|
|
91
|
+
print "F"
|
|
92
|
+
end
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Function
|
|
96
|
+
```
|
|
97
|
+
function add(a, b)
|
|
98
|
+
return a + b
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
result = add(3, 4)
|
|
102
|
+
print result
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### For loop
|
|
106
|
+
```
|
|
107
|
+
fruits = ["apple", "banana", "cherry"]
|
|
108
|
+
for fruit in fruits
|
|
109
|
+
print "I like {fruit}"
|
|
110
|
+
end
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### While loop
|
|
114
|
+
```
|
|
115
|
+
count = 0
|
|
116
|
+
while count < 5
|
|
117
|
+
count = count + 1
|
|
118
|
+
end
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Try / catch
|
|
122
|
+
```
|
|
123
|
+
try
|
|
124
|
+
data = http.get("https://api.example.com/data")
|
|
125
|
+
print data.title
|
|
126
|
+
catch err
|
|
127
|
+
print "Error: {err}"
|
|
128
|
+
end
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Objects and lists
|
|
132
|
+
```
|
|
133
|
+
user = { name: "João" age: 30 city: "Lisbon" }
|
|
134
|
+
scores = [85, 92, 78]
|
|
135
|
+
print user.name
|
|
136
|
+
print scores.length
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### String interpolation
|
|
140
|
+
```
|
|
141
|
+
msg = "Name: {user.name}, Age: {user.age}"
|
|
142
|
+
print msg
|
|
143
|
+
print "Pi is {math.pi}"
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Import system
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
# Import all functions from a file by name
|
|
152
|
+
use "./utils.future"
|
|
153
|
+
result = formatName("Alice")
|
|
154
|
+
|
|
155
|
+
# Import as a namespace
|
|
156
|
+
use "./math.future" as m
|
|
157
|
+
result = m.add(10, 20)
|
|
158
|
+
|
|
159
|
+
# Import an npm package as a namespace
|
|
160
|
+
use "date-fns" as df
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Imported `.future` files must contain only top-level function declarations. They compile to ES module exports automatically.
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## Capability namespaces
|
|
168
|
+
|
|
169
|
+
No `async`/`await` needed — the compiler handles it. Any namespace call switches the program to async mode automatically.
|
|
170
|
+
|
|
171
|
+
### `ai`
|
|
172
|
+
```
|
|
173
|
+
answer = ai.ask("What is the capital of France?")
|
|
174
|
+
reply = ai.chat([{ role: "user" content: "Hello" }])
|
|
175
|
+
embed = ai.embed("text to embed")
|
|
176
|
+
ai.configure("openai", "sk-...")
|
|
177
|
+
ai.configure("ollama")
|
|
178
|
+
|
|
179
|
+
stream ai.ask("Tell me a story")
|
|
180
|
+
print chunk
|
|
181
|
+
end
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### `http`
|
|
185
|
+
```
|
|
186
|
+
data = http.get("https://api.example.com/todos/1")
|
|
187
|
+
print data.title
|
|
188
|
+
|
|
189
|
+
res = http.post("https://api.example.com/items", { name: "Widget" price: 9.99 })
|
|
190
|
+
print res.id
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### `mqtt`
|
|
194
|
+
```
|
|
195
|
+
mqtt.publish("home/light", "on")
|
|
196
|
+
|
|
197
|
+
on mqtt "home/temp"
|
|
198
|
+
print "Temperature: {message}"
|
|
199
|
+
end
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### `tts`
|
|
203
|
+
```
|
|
204
|
+
tts.speak("Hello from Future!")
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### `memory`
|
|
208
|
+
```
|
|
209
|
+
memory.set("key", "value")
|
|
210
|
+
val = memory.get("key")
|
|
211
|
+
memory.delete("key")
|
|
212
|
+
results = memory.search("query")
|
|
213
|
+
memory.forget() # clear all
|
|
214
|
+
memory.forget("prefix") # clear matching keys
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### `schedule`
|
|
218
|
+
```
|
|
219
|
+
every "30m"
|
|
220
|
+
data = http.get("https://api.example.com/stats")
|
|
221
|
+
print data.count
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# schedule.once and schedule.cron also available
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### `http`, `system`, `rag`, `vision`, `home`, `device`
|
|
228
|
+
See [README.md](README.md) for full API tables.
|
|
229
|
+
|
|
230
|
+
### `math`
|
|
231
|
+
```
|
|
232
|
+
print math.round(3.7) # 4
|
|
233
|
+
print math.sqrt(16) # 4
|
|
234
|
+
print math.pi # 3.14159…
|
|
235
|
+
print math.random() # 0–1
|
|
236
|
+
print math.max(1, 5, 3) # 5
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### `len` (built-in, not a namespace)
|
|
240
|
+
```
|
|
241
|
+
print len([1, 2, 3]) # 3
|
|
242
|
+
print len("hello") # 5
|
|
243
|
+
print len({ a: 1 b: 2 }) # 2
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## Agents
|
|
249
|
+
|
|
250
|
+
```
|
|
251
|
+
agent support
|
|
252
|
+
use rag
|
|
253
|
+
use memory
|
|
254
|
+
|
|
255
|
+
docs = rag.query(goal)
|
|
256
|
+
memory.set("last", goal)
|
|
257
|
+
return docs
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
answer = support("How do I reset the device?")
|
|
261
|
+
print answer
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
`goal` is the implicit parameter. `use` inside an agent declares capabilities (documentation only — no generated code).
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
## Common mistakes
|
|
269
|
+
|
|
270
|
+
| Wrong | Correct |
|
|
271
|
+
|-------|---------|
|
|
272
|
+
| `if (x > 0) {` | `if x > 0` |
|
|
273
|
+
| `end if` | `end` |
|
|
274
|
+
| `elif` | `else if` |
|
|
275
|
+
| `&&` / `\|\|` / `!` | `and` / `or` / `not` |
|
|
276
|
+
| `x++` | `x = x + 1` |
|
|
277
|
+
| `x += 1` | `x = x + 1` |
|
|
278
|
+
| `// comment` | `# comment` |
|
|
279
|
+
| `import "./utils.js"` | `use "./utils.future"` |
|
|
280
|
+
| `let x = 5` | `x = 5` |
|
|
281
|
+
| `function f() { }` | `function f()` + body + `end` |
|
|
282
|
+
|
|
283
|
+
---
|
|
284
|
+
|
|
285
|
+
## What compiles to what
|
|
286
|
+
|
|
287
|
+
```future
|
|
288
|
+
x = 1
|
|
289
|
+
```
|
|
290
|
+
```js
|
|
291
|
+
let x;
|
|
292
|
+
x = 1;
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
```future
|
|
296
|
+
if x > 0
|
|
297
|
+
print "positive"
|
|
298
|
+
else if x < 0
|
|
299
|
+
print "negative"
|
|
300
|
+
else
|
|
301
|
+
print "zero"
|
|
302
|
+
end
|
|
303
|
+
```
|
|
304
|
+
```js
|
|
305
|
+
if (x > 0) {
|
|
306
|
+
console.log("positive");
|
|
307
|
+
} else if (x < 0) {
|
|
308
|
+
console.log("negative");
|
|
309
|
+
} else {
|
|
310
|
+
console.log("zero");
|
|
311
|
+
}
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
```future
|
|
315
|
+
answer = ai.ask("Hello?")
|
|
316
|
+
```
|
|
317
|
+
```js
|
|
318
|
+
import { runtime as __rt } from "future-lang/runtime";
|
|
319
|
+
let answer;
|
|
320
|
+
answer = await __rt.ai.ask("Hello?");
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
```future
|
|
324
|
+
use "./utils.future"
|
|
325
|
+
name = formatName("Alice")
|
|
326
|
+
```
|
|
327
|
+
```js
|
|
328
|
+
import { formatName } from "./utils.js";
|
|
329
|
+
let name;
|
|
330
|
+
name = formatName("Alice");
|
|
331
|
+
```
|
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ Future reads like plain English, runs on Node.js, and gives every program built-
|
|
|
9
9
|
## Quick start
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
npm install future-lang
|
|
12
|
+
npm install -g future-lang
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
```future
|
|
@@ -331,8 +331,49 @@ Open `future-playground.html` in any browser for a live editor with 11 built-in
|
|
|
331
331
|
## CLI
|
|
332
332
|
|
|
333
333
|
```bash
|
|
334
|
+
npm install -g future-lang
|
|
335
|
+
|
|
336
|
+
future --version # show version
|
|
337
|
+
future new myapp # create a new project
|
|
334
338
|
future run program.future # compile + run
|
|
335
|
-
future compile program.future #
|
|
339
|
+
future compile program.future # compile to JavaScript
|
|
340
|
+
future check program.future # syntax check only
|
|
341
|
+
future fmt program.future # format source in-place
|
|
342
|
+
future playground # launch the interactive playground
|
|
343
|
+
future doctor # check your environment
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
---
|
|
347
|
+
|
|
348
|
+
## Import system
|
|
349
|
+
|
|
350
|
+
Split code across files with `use`:
|
|
351
|
+
|
|
352
|
+
```future
|
|
353
|
+
# utils.future
|
|
354
|
+
function formatName(name)
|
|
355
|
+
return "User: {name}"
|
|
356
|
+
end
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
```future
|
|
360
|
+
# main.future
|
|
361
|
+
|
|
362
|
+
# import all functions by name
|
|
363
|
+
use "./utils.future"
|
|
364
|
+
print formatName("Alice")
|
|
365
|
+
|
|
366
|
+
# import as a namespace
|
|
367
|
+
use "./math.future" as m
|
|
368
|
+
result = m.add(10, 20)
|
|
369
|
+
print result
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
Compiles to standard ES module imports:
|
|
373
|
+
|
|
374
|
+
```js
|
|
375
|
+
import { formatName } from "./utils.js";
|
|
376
|
+
import * as m from "./math.js";
|
|
336
377
|
```
|
|
337
378
|
|
|
338
379
|
---
|
|
@@ -365,6 +406,6 @@ Source (.future) → Lexer → Parser → Generator → JavaScript
|
|
|
365
406
|
|
|
366
407
|
## Documentation
|
|
367
408
|
|
|
368
|
-
- [ARCHITECTURE.md](ARCHITECTURE.md) — compiler pipeline, folder structure, AST node types
|
|
369
|
-
- [ROADMAP.md](ROADMAP.md) — feature status and priorities
|
|
370
|
-
- [MIGRATION.md](MIGRATION.md) — changelog, what changed between versions
|
|
409
|
+
- [ARCHITECTURE.md](https://github.com/humolot/future-lang/blob/main/ARCHITECTURE.md) — compiler pipeline, folder structure, AST node types
|
|
410
|
+
- [ROADMAP.md](https://github.com/humolot/future-lang/blob/main/ROADMAP.md) — feature status and priorities
|
|
411
|
+
- [MIGRATION.md](https://github.com/humolot/future-lang/blob/main/MIGRATION.md) — changelog, what changed between versions
|
package/package.json
CHANGED
package/runtime/index.js
CHANGED
|
@@ -24,7 +24,37 @@ const MODULE_NAMES = [
|
|
|
24
24
|
'memory', 'schedule', 'system', 'device', 'math',
|
|
25
25
|
];
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
const _base = { ai, http, mqtt, tts, rag, vision, home, memory, schedule, system, device, math };
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* When FUTURE_DEBUG=1, wrap every namespace method with timing/logging.
|
|
31
|
+
* Non-function properties (constants like math.pi) pass through unchanged.
|
|
32
|
+
*/
|
|
33
|
+
function wrapDebug(base) {
|
|
34
|
+
const wrapped = {};
|
|
35
|
+
for (const [ns, mod] of Object.entries(base)) {
|
|
36
|
+
wrapped[ns] = {};
|
|
37
|
+
for (const [key, val] of Object.entries(mod)) {
|
|
38
|
+
if (typeof val !== 'function') { wrapped[ns][key] = val; continue; }
|
|
39
|
+
wrapped[ns][key] = async (...args) => {
|
|
40
|
+
const preview = args.length ? String(JSON.stringify(args[0])).slice(0, 60) : '';
|
|
41
|
+
process.stderr.write(`\x1b[90m[debug] ${ns}.${key}(${preview}) …\x1b[0m\n`);
|
|
42
|
+
const t = Date.now();
|
|
43
|
+
try {
|
|
44
|
+
const result = await val(...args);
|
|
45
|
+
process.stderr.write(`\x1b[90m[debug] ${ns}.${key} ✓ ${Date.now() - t}ms\x1b[0m\n`);
|
|
46
|
+
return result;
|
|
47
|
+
} catch (err) {
|
|
48
|
+
process.stderr.write(`\x1b[31m[debug] ${ns}.${key} ✗ ${Date.now() - t}ms — ${err.message}\x1b[0m\n`);
|
|
49
|
+
throw err;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return wrapped;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const runtime = process.env.FUTURE_DEBUG === '1' ? wrapDebug(_base) : _base;
|
|
28
58
|
|
|
29
59
|
// input(prompt) — reads a line from stdin (CLI programs).
|
|
30
60
|
runtime.input = async (prompt = '') => {
|
|
@@ -395,7 +425,7 @@ runtime.listFunctions = (mod) => {
|
|
|
395
425
|
* Suitable for AI agent discovery or documentation generation.
|
|
396
426
|
*/
|
|
397
427
|
runtime.describe = () => ({
|
|
398
|
-
version: '0.
|
|
428
|
+
version: '0.4.0',
|
|
399
429
|
modules: [...MODULE_NAMES],
|
|
400
430
|
manifest,
|
|
401
431
|
});
|
package/src/cli.js
CHANGED
|
@@ -21,7 +21,7 @@ import { compile, tokenize, parse } from './index.js';
|
|
|
21
21
|
import { format } from './formatter.js';
|
|
22
22
|
import { FutureError } from './errors.js';
|
|
23
23
|
|
|
24
|
-
const VERSION = '0.
|
|
24
|
+
const VERSION = '0.4.0';
|
|
25
25
|
const PROJECT_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
26
26
|
const RUNTIME_INDEX = join(PROJECT_ROOT, 'runtime', 'index.js');
|
|
27
27
|
|
|
@@ -41,10 +41,17 @@ Usage:
|
|
|
41
41
|
Import system:
|
|
42
42
|
use "./utils.future" Import all functions from a file
|
|
43
43
|
use "./math.future" as math Import as a namespace (math.add, math.pi …)
|
|
44
|
+
use "lodash" as _ Import an npm package as a namespace
|
|
45
|
+
|
|
46
|
+
Flags:
|
|
47
|
+
future run --debug <file> Show timing for every namespace call
|
|
44
48
|
`;
|
|
45
49
|
|
|
46
50
|
async function main(argv) {
|
|
47
|
-
const
|
|
51
|
+
const debug = argv.includes('--debug');
|
|
52
|
+
if (debug) process.env.FUTURE_DEBUG = '1';
|
|
53
|
+
const rest = argv.filter((a) => a !== '--debug');
|
|
54
|
+
const [command, arg] = rest;
|
|
48
55
|
switch (command) {
|
|
49
56
|
case 'run': return cmdRun(arg);
|
|
50
57
|
case 'compile': return cmdCompile(arg);
|
|
@@ -138,6 +145,7 @@ function compileDepsToTemp(sourcePath, sourceText, tempDir, pathMap = new Map())
|
|
|
138
145
|
const uses = findUseStatements(sourceText);
|
|
139
146
|
for (const { path: relPath } of uses) {
|
|
140
147
|
if (pathMap.has(relPath)) continue; // already compiled
|
|
148
|
+
if (!relPath.startsWith('./') && !relPath.startsWith('../')) continue; // npm module
|
|
141
149
|
const depAbsPath = resolve(dirname(sourcePath), relPath);
|
|
142
150
|
if (!existsSync(depAbsPath)) continue;
|
|
143
151
|
const depSource = readFileSync(depAbsPath, 'utf8');
|
|
@@ -163,6 +171,7 @@ function compileDepModule(source, sourcePath, tempDir, pathMap) {
|
|
|
163
171
|
// Ensure transitive deps are compiled first so pathMap is populated.
|
|
164
172
|
for (const { path: relPath } of uses) {
|
|
165
173
|
if (pathMap.has(relPath)) continue;
|
|
174
|
+
if (!relPath.startsWith('./') && !relPath.startsWith('../')) continue; // npm module
|
|
166
175
|
const depAbsPath = resolve(dirname(sourcePath), relPath);
|
|
167
176
|
if (!existsSync(depAbsPath)) continue;
|
|
168
177
|
const depSrc = readFileSync(depAbsPath, 'utf8');
|
|
@@ -199,6 +208,7 @@ function cmdCompile(file) {
|
|
|
199
208
|
// Compile each imported .future dep as a module next to its source.
|
|
200
209
|
const uses = findUseStatements(source);
|
|
201
210
|
for (const { path: relPath } of uses) {
|
|
211
|
+
if (!relPath.startsWith('./') && !relPath.startsWith('../')) continue; // npm module
|
|
202
212
|
const depAbsPath = resolve(outDir, relPath);
|
|
203
213
|
if (!existsSync(depAbsPath)) {
|
|
204
214
|
process.stderr.write(`warning: imported file not found: ${relPath}\n`);
|
package/src/generator.js
CHANGED
|
@@ -62,6 +62,12 @@ export class Generator {
|
|
|
62
62
|
lines.push('');
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
// __safe wraps async event handlers so errors are logged instead of crashing silently.
|
|
66
|
+
if (usesHandlers(program)) {
|
|
67
|
+
lines.push('const __safe = (ns, fn) => async (...a) => { try { return await fn(...a); } catch (e) { console.error(`[future:${ns}]`, e.message); } };');
|
|
68
|
+
lines.push('');
|
|
69
|
+
}
|
|
70
|
+
|
|
65
71
|
// Emit __len helper only when len() is actually used — keeps simple programs clean.
|
|
66
72
|
if (usesBuiltin(program, 'len')) {
|
|
67
73
|
lines.push('function __len(x) { return x == null ? 0 : (x.length ?? Object.keys(x).length); }');
|
|
@@ -80,11 +86,17 @@ export class Generator {
|
|
|
80
86
|
|
|
81
87
|
/** Emit an ES `import` for a `use` statement. */
|
|
82
88
|
genUseStatement(node) {
|
|
83
|
-
const
|
|
89
|
+
const isRelative = node.path.startsWith('./') || node.path.startsWith('../');
|
|
90
|
+
const jsPath = isRelative ? node.path.replace(/\.future$/, '.js') : node.path;
|
|
84
91
|
const resolved = this.pathMap.get(node.path) ?? jsPath;
|
|
92
|
+
|
|
85
93
|
if (node.alias) {
|
|
86
94
|
return `import * as ${node.alias} from ${JSON.stringify(resolved)};`;
|
|
87
95
|
}
|
|
96
|
+
if (!isRelative) {
|
|
97
|
+
// npm module without alias — side-effect import
|
|
98
|
+
return `import ${JSON.stringify(resolved)};`;
|
|
99
|
+
}
|
|
88
100
|
const names = this.importedNames.get(node.path) ?? [];
|
|
89
101
|
if (names.length > 0) {
|
|
90
102
|
return `import { ${names.join(', ')} } from ${JSON.stringify(resolved)};`;
|
|
@@ -111,7 +123,13 @@ export class Generator {
|
|
|
111
123
|
out += this.genBody(node.consequent, depth + 1);
|
|
112
124
|
out += `\n${pad}}`;
|
|
113
125
|
if (node.alternate) {
|
|
114
|
-
|
|
126
|
+
// Single chained IfStatement → `else if (...)` without extra braces.
|
|
127
|
+
if (node.alternate.length === 1 && node.alternate[0].type === NodeType.IfStatement) {
|
|
128
|
+
const elseIf = this.genStatement(node.alternate[0], depth);
|
|
129
|
+
out += ` else ${elseIf.trimStart()}`;
|
|
130
|
+
} else {
|
|
131
|
+
out += ` else {\n${this.genBody(node.alternate, depth + 1)}\n${pad}}`;
|
|
132
|
+
}
|
|
115
133
|
}
|
|
116
134
|
return out;
|
|
117
135
|
}
|
|
@@ -172,7 +190,7 @@ export class Generator {
|
|
|
172
190
|
const args = call.arguments.map((a) => this.genExpression(a)).join(', ');
|
|
173
191
|
const sep = args ? ', ' : '';
|
|
174
192
|
const inner = this.genBody(node.body, depth + 1);
|
|
175
|
-
return `${pad}await __rt.${ns}.stream(${args}${sep}async (chunk) => {\n${inner}\n${pad}});`;
|
|
193
|
+
return `${pad}await __rt.${ns}.stream(${args}${sep}__safe("stream", async (chunk) => {\n${inner}\n${pad}}));`;
|
|
176
194
|
}
|
|
177
195
|
|
|
178
196
|
case NodeType.TryStatement: {
|
|
@@ -201,18 +219,19 @@ export class Generator {
|
|
|
201
219
|
|
|
202
220
|
case NodeType.OnStatement: {
|
|
203
221
|
// `on mqtt "topic" ... end`
|
|
204
|
-
//
|
|
222
|
+
// → await __rt.<source>.subscribe(<channel>, __safe("<source>", async (message) => { ... }))
|
|
205
223
|
const inner = this.genBody(node.body, depth + 1);
|
|
206
224
|
const chan = this.genExpression(node.channel);
|
|
207
|
-
|
|
225
|
+
const ns = JSON.stringify(node.source);
|
|
226
|
+
return `${pad}await __rt.${node.source}.subscribe(${chan}, __safe(${ns}, async (message) => {\n${inner}\n${pad}}));`;
|
|
208
227
|
}
|
|
209
228
|
|
|
210
229
|
case NodeType.EveryStatement: {
|
|
211
230
|
// `every "30m" ... end`
|
|
212
|
-
//
|
|
231
|
+
// → await __rt.schedule.every(<interval>, __safe("schedule", async () => { ... }))
|
|
213
232
|
const inner = this.genBody(node.body, depth + 1);
|
|
214
233
|
const interval = this.genExpression(node.interval);
|
|
215
|
-
return `${pad}await __rt.schedule.every(${interval}, async () => {\n${inner}\n${pad}});`;
|
|
234
|
+
return `${pad}await __rt.schedule.every(${interval}, __safe("schedule", async () => {\n${inner}\n${pad}}));`;
|
|
216
235
|
}
|
|
217
236
|
|
|
218
237
|
default:
|
|
@@ -390,6 +409,22 @@ function usesRuntime(node, useAliases = new Set()) {
|
|
|
390
409
|
return false;
|
|
391
410
|
}
|
|
392
411
|
|
|
412
|
+
/** True if the program has any async event handlers (on/every/stream). */
|
|
413
|
+
function usesHandlers(node) {
|
|
414
|
+
if (!node || typeof node !== 'object') return false;
|
|
415
|
+
if (
|
|
416
|
+
node.type === NodeType.OnStatement ||
|
|
417
|
+
node.type === NodeType.EveryStatement ||
|
|
418
|
+
node.type === NodeType.StreamStatement
|
|
419
|
+
) return true;
|
|
420
|
+
for (const key of Object.keys(node)) {
|
|
421
|
+
const v = node[key];
|
|
422
|
+
if (Array.isArray(v)) { if (v.some(usesHandlers)) return true; }
|
|
423
|
+
else if (v && typeof v === 'object' && v.type) { if (usesHandlers(v)) return true; }
|
|
424
|
+
}
|
|
425
|
+
return false;
|
|
426
|
+
}
|
|
427
|
+
|
|
393
428
|
/** Walk the AST and check if a specific built-in function name is called. */
|
|
394
429
|
function usesBuiltin(node, name) {
|
|
395
430
|
if (!node || typeof node !== 'object') return false;
|
package/src/index.js
CHANGED
|
@@ -31,6 +31,8 @@ export function compile(source, options = {}) {
|
|
|
31
31
|
if (options.resolveSource) {
|
|
32
32
|
for (const stmt of ast.body) {
|
|
33
33
|
if (stmt.type !== 'UseStatement' || stmt.alias) continue;
|
|
34
|
+
// Skip npm module imports — no source to resolve.
|
|
35
|
+
if (!stmt.path.startsWith('./') && !stmt.path.startsWith('../')) continue;
|
|
34
36
|
try {
|
|
35
37
|
const importedSrc = options.resolveSource(stmt.path);
|
|
36
38
|
if (importedSrc) {
|
package/src/parser.js
CHANGED
|
@@ -16,6 +16,12 @@ import * as AST from './ast.js';
|
|
|
16
16
|
*/
|
|
17
17
|
const EXPR_TERMINATORS = new Set(['END', 'ELSE', 'CATCH', 'EOF']);
|
|
18
18
|
|
|
19
|
+
/** Built-in namespace names that cannot be redefined by user code. */
|
|
20
|
+
const RESERVED_NAMESPACES = new Set([
|
|
21
|
+
'ai', 'http', 'mqtt', 'tts', 'rag', 'vision', 'home',
|
|
22
|
+
'memory', 'schedule', 'system', 'device', 'math',
|
|
23
|
+
]);
|
|
24
|
+
|
|
19
25
|
export class Parser {
|
|
20
26
|
/** @param {import('./lexer.js').Token[]} tokens */
|
|
21
27
|
constructor(tokens) {
|
|
@@ -113,27 +119,51 @@ export class Parser {
|
|
|
113
119
|
|
|
114
120
|
parseAssignment() {
|
|
115
121
|
const name = this.advance(); // IDENTIFIER
|
|
122
|
+
if (RESERVED_NAMESPACES.has(name.value)) {
|
|
123
|
+
throw new FutureError(
|
|
124
|
+
`'${name.value}' is a reserved namespace and cannot be reassigned`,
|
|
125
|
+
name.line, name.column, 'parse',
|
|
126
|
+
);
|
|
127
|
+
}
|
|
116
128
|
this.expect('ASSIGN', "'='");
|
|
117
129
|
const value = this.parseExpression();
|
|
118
130
|
return AST.Assignment(name.value, value, name.line, name.column);
|
|
119
131
|
}
|
|
120
132
|
|
|
121
|
-
|
|
133
|
+
/**
|
|
134
|
+
* `if cond ... [else if cond ...]* [else ...] end`
|
|
135
|
+
* @param {boolean} isChained True when parsing an `else if` branch — the
|
|
136
|
+
* outer `if` owns the single `end`, so this call must NOT consume it.
|
|
137
|
+
*/
|
|
138
|
+
parseIf(isChained = false) {
|
|
122
139
|
const kw = this.advance(); // IF
|
|
123
140
|
const condition = this.parseExpression();
|
|
124
|
-
const consequent = this.parseBlock(['ELSE', 'END']);
|
|
141
|
+
const consequent = this.parseBlock(['ELSE', 'END'], 'if');
|
|
125
142
|
let alternate = null;
|
|
126
143
|
if (this.check('ELSE')) {
|
|
127
|
-
this.advance();
|
|
128
|
-
|
|
144
|
+
this.advance(); // ELSE
|
|
145
|
+
if (this.check('IF')) {
|
|
146
|
+
// else if — recurse; the chained call skips its own `end`
|
|
147
|
+
alternate = [this.parseIf(true)];
|
|
148
|
+
} else {
|
|
149
|
+
alternate = this.parseBlock(['END'], 'else');
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (!isChained) {
|
|
153
|
+
this.expect('END', "'end' to close 'if'");
|
|
129
154
|
}
|
|
130
|
-
this.expect('END', "'end'");
|
|
131
155
|
return AST.IfStatement(condition, consequent, alternate, kw.line, kw.column);
|
|
132
156
|
}
|
|
133
157
|
|
|
134
158
|
parseFunction() {
|
|
135
159
|
const kw = this.advance(); // FUNCTION
|
|
136
160
|
const name = this.expect('IDENTIFIER', 'function name');
|
|
161
|
+
if (RESERVED_NAMESPACES.has(name.value)) {
|
|
162
|
+
throw new FutureError(
|
|
163
|
+
`'${name.value}' is a reserved namespace and cannot be used as a function name`,
|
|
164
|
+
name.line, name.column, 'parse',
|
|
165
|
+
);
|
|
166
|
+
}
|
|
137
167
|
this.expect('LPAREN', "'('");
|
|
138
168
|
const params = [];
|
|
139
169
|
if (!this.check('RPAREN')) {
|
|
@@ -142,8 +172,8 @@ export class Parser {
|
|
|
142
172
|
} while (this.match('COMMA'));
|
|
143
173
|
}
|
|
144
174
|
this.expect('RPAREN', "')'");
|
|
145
|
-
const body = this.parseBlock(['END']);
|
|
146
|
-
this.expect('END', "'end'");
|
|
175
|
+
const body = this.parseBlock(['END'], 'function');
|
|
176
|
+
this.expect('END', "'end' to close 'function'");
|
|
147
177
|
return AST.FunctionDeclaration(name.value, params, body, kw.line, kw.column);
|
|
148
178
|
}
|
|
149
179
|
|
|
@@ -169,8 +199,8 @@ export class Parser {
|
|
|
169
199
|
const variable = this.expect('IDENTIFIER', 'loop variable name');
|
|
170
200
|
this.expect('IN', "'in'");
|
|
171
201
|
const iterable = this.parseExpression();
|
|
172
|
-
const body = this.parseBlock(['END']);
|
|
173
|
-
this.expect('END', "'end'");
|
|
202
|
+
const body = this.parseBlock(['END'], 'for');
|
|
203
|
+
this.expect('END', "'end' to close 'for'");
|
|
174
204
|
return AST.ForStatement(variable.value, iterable, body, kw.line, kw.column);
|
|
175
205
|
}
|
|
176
206
|
|
|
@@ -179,11 +209,11 @@ export class Parser {
|
|
|
179
209
|
*/
|
|
180
210
|
parseTry() {
|
|
181
211
|
const kw = this.advance(); // TRY
|
|
182
|
-
const body = this.parseBlock(['CATCH']);
|
|
183
|
-
this.expect('CATCH', "'catch'");
|
|
212
|
+
const body = this.parseBlock(['CATCH'], 'try');
|
|
213
|
+
this.expect('CATCH', "'catch' after 'try' block");
|
|
184
214
|
const errVar = this.expect('IDENTIFIER', 'error variable name');
|
|
185
|
-
const catchBody = this.parseBlock(['END']);
|
|
186
|
-
this.expect('END', "'end'");
|
|
215
|
+
const catchBody = this.parseBlock(['END'], 'catch');
|
|
216
|
+
this.expect('END', "'end' to close 'try'");
|
|
187
217
|
return AST.TryStatement(body, errVar.value, catchBody, kw.line, kw.column);
|
|
188
218
|
}
|
|
189
219
|
|
|
@@ -211,7 +241,7 @@ export class Parser {
|
|
|
211
241
|
body.push(this.parseStatement());
|
|
212
242
|
}
|
|
213
243
|
}
|
|
214
|
-
this.expect('END', "'end'");
|
|
244
|
+
this.expect('END', "'end' to close 'agent'");
|
|
215
245
|
return AST.AgentDeclaration(name.value, capabilities, body, kw.line, kw.column);
|
|
216
246
|
}
|
|
217
247
|
|
|
@@ -221,8 +251,8 @@ export class Parser {
|
|
|
221
251
|
parseWhile() {
|
|
222
252
|
const kw = this.advance(); // WHILE
|
|
223
253
|
const condition = this.parseExpression();
|
|
224
|
-
const body = this.parseBlock(['END']);
|
|
225
|
-
this.expect('END', "'end'");
|
|
254
|
+
const body = this.parseBlock(['END'], 'while');
|
|
255
|
+
this.expect('END', "'end' to close 'while'");
|
|
226
256
|
return AST.WhileStatement(condition, body, kw.line, kw.column);
|
|
227
257
|
}
|
|
228
258
|
|
|
@@ -235,8 +265,8 @@ export class Parser {
|
|
|
235
265
|
parseStream() {
|
|
236
266
|
const kw = this.advance(); // STREAM
|
|
237
267
|
const call = this.parseExpression();
|
|
238
|
-
const body = this.parseBlock(['END']);
|
|
239
|
-
this.expect('END', "'end'");
|
|
268
|
+
const body = this.parseBlock(['END'], 'stream');
|
|
269
|
+
this.expect('END', "'end' to close 'stream'");
|
|
240
270
|
return AST.StreamStatement(call, body, kw.line, kw.column);
|
|
241
271
|
}
|
|
242
272
|
|
|
@@ -249,8 +279,8 @@ export class Parser {
|
|
|
249
279
|
const kw = this.advance(); // ON
|
|
250
280
|
const source = this.expect('IDENTIFIER', 'event source name (e.g. mqtt)');
|
|
251
281
|
const channel = this.parseExpression();
|
|
252
|
-
const body = this.parseBlock(['END']);
|
|
253
|
-
this.expect('END', "'end'");
|
|
282
|
+
const body = this.parseBlock(['END'], 'on');
|
|
283
|
+
this.expect('END', "'end' to close 'on'");
|
|
254
284
|
return AST.OnStatement(source.value, channel, body, kw.line, kw.column);
|
|
255
285
|
}
|
|
256
286
|
|
|
@@ -262,8 +292,8 @@ export class Parser {
|
|
|
262
292
|
parseEvery() {
|
|
263
293
|
const kw = this.advance(); // EVERY
|
|
264
294
|
const interval = this.parseExpression();
|
|
265
|
-
const body = this.parseBlock(['END']);
|
|
266
|
-
this.expect('END', "'end'");
|
|
295
|
+
const body = this.parseBlock(['END'], 'every');
|
|
296
|
+
this.expect('END', "'end' to close 'every'");
|
|
267
297
|
return AST.EveryStatement(interval, body, kw.line, kw.column);
|
|
268
298
|
}
|
|
269
299
|
|
|
@@ -271,7 +301,7 @@ export class Parser {
|
|
|
271
301
|
* Collect statements until one of `terminators` (or EOF) is next.
|
|
272
302
|
* Throws if EOF is reached before a terminator (e.g. a missing `end`).
|
|
273
303
|
*/
|
|
274
|
-
parseBlock(terminators) {
|
|
304
|
+
parseBlock(terminators, openedBy = null) {
|
|
275
305
|
const statements = [];
|
|
276
306
|
while (!this.check('EOF') && !terminators.includes(this.peek().type)) {
|
|
277
307
|
statements.push(this.parseStatement());
|
|
@@ -279,8 +309,9 @@ export class Parser {
|
|
|
279
309
|
if (this.check('EOF')) {
|
|
280
310
|
const tok = this.peek();
|
|
281
311
|
const expected = terminators.map((t) => `'${t.toLowerCase()}'`).join(' or ');
|
|
312
|
+
const hint = openedBy ? ` to close '${openedBy}'` : '';
|
|
282
313
|
throw new FutureError(
|
|
283
|
-
`Unexpected end of file
|
|
314
|
+
`Unexpected end of file — expected ${expected}${hint}`,
|
|
284
315
|
tok.line, tok.column, 'parse',
|
|
285
316
|
);
|
|
286
317
|
}
|