future-lang 0.3.2 → 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.
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "future-lang",
3
- "version": "0.3.2",
3
+ "version": "0.4.0",
4
4
  "description": "Future — a small programming language that transpiles to JavaScript, with a capability runtime (HTTP/AI/MQTT/TTS).",
5
5
  "type": "module",
6
6
  "bin": {
package/runtime/index.js CHANGED
@@ -24,7 +24,37 @@ const MODULE_NAMES = [
24
24
  'memory', 'schedule', 'system', 'device', 'math',
25
25
  ];
26
26
 
27
- export const runtime = { ai, http, mqtt, tts, rag, vision, home, memory, schedule, system, device, math };
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.2.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.3.2';
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 [command, arg] = argv;
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 jsPath = node.path.replace(/\.future$/, '.js');
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
- out += ` else {\n${this.genBody(node.alternate, depth + 1)}\n${pad}}`;
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
- // Compiles to: await __rt.<source>.subscribe(<channel>, async (message) => { ... })
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
- return `${pad}await __rt.${node.source}.subscribe(${chan}, async (message) => {\n${inner}\n${pad}});`;
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
- // Compiles to: await __rt.schedule.every(<interval>, async () => { ... })
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
- parseIf() {
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
- alternate = this.parseBlock(['END']);
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, expected ${expected}`,
314
+ `Unexpected end of file expected ${expected}${hint}`,
284
315
  tok.line, tok.column, 'parse',
285
316
  );
286
317
  }