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.
Files changed (47) hide show
  1. package/ARCHITECTURE.md +424 -0
  2. package/MIGRATION.md +365 -0
  3. package/README.md +370 -0
  4. package/ROADMAP.md +263 -0
  5. package/examples/adult.future +8 -0
  6. package/examples/api.future +11 -0
  7. package/examples/assistant.future +8 -0
  8. package/examples/browser-demo.html +164 -0
  9. package/examples/greet.future +7 -0
  10. package/examples/hello.future +1 -0
  11. package/examples/math.future +8 -0
  12. package/examples/mini-app.html +301 -0
  13. package/examples/smarthome.future +10 -0
  14. package/future-browser.js +102 -0
  15. package/future-playground.html +650 -0
  16. package/package.json +27 -0
  17. package/runtime/ai.js +92 -0
  18. package/runtime/browser.js +458 -0
  19. package/runtime/device.js +36 -0
  20. package/runtime/home.js +19 -0
  21. package/runtime/http.js +32 -0
  22. package/runtime/index.js +403 -0
  23. package/runtime/lsp-metadata.js +104 -0
  24. package/runtime/math.js +16 -0
  25. package/runtime/memory.js +61 -0
  26. package/runtime/mqtt.js +49 -0
  27. package/runtime/providers/anthropic.js +59 -0
  28. package/runtime/providers/index.js +93 -0
  29. package/runtime/providers/openai-compat.js +85 -0
  30. package/runtime/providers/util.js +70 -0
  31. package/runtime/rag/chunker.js +65 -0
  32. package/runtime/rag/pipeline.js +86 -0
  33. package/runtime/rag/vector-store.js +119 -0
  34. package/runtime/rag.js +94 -0
  35. package/runtime/schedule.js +77 -0
  36. package/runtime/system.js +101 -0
  37. package/runtime/tts.js +38 -0
  38. package/runtime/vision.js +85 -0
  39. package/server.js +42 -0
  40. package/src/ast.js +202 -0
  41. package/src/cli.js +391 -0
  42. package/src/errors.js +21 -0
  43. package/src/formatter.js +48 -0
  44. package/src/generator.js +457 -0
  45. package/src/index.js +48 -0
  46. package/src/lexer.js +248 -0
  47. package/src/parser.js +469 -0
package/src/cli.js ADDED
@@ -0,0 +1,391 @@
1
+ #!/usr/bin/env node
2
+ // cli.js — The `future` command.
3
+ //
4
+ // future run <file.future> Compile then execute
5
+ // future compile <file.future> Compile to <file>.js next to the source
6
+ // future new <name> Create a new project scaffold
7
+ // future check <file.future> Syntax-check without running
8
+ // future fmt <file.future> Format source code in-place
9
+ // future playground Launch the interactive playground
10
+ // future doctor Check your environment
11
+ // future help | --help
12
+ // future version | --version
13
+
14
+ import { readFileSync, writeFileSync, unlinkSync, mkdirSync, existsSync } from 'node:fs';
15
+ import { basename, dirname, extname, join, relative, resolve } from 'node:path';
16
+ import { fileURLToPath, pathToFileURL } from 'node:url';
17
+ import { tmpdir } from 'node:os';
18
+ import process from 'node:process';
19
+
20
+ import { compile, tokenize, parse } from './index.js';
21
+ import { format } from './formatter.js';
22
+ import { FutureError } from './errors.js';
23
+
24
+ const VERSION = '0.3.0';
25
+ const PROJECT_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
26
+ const RUNTIME_INDEX = join(PROJECT_ROOT, 'runtime', 'index.js');
27
+
28
+ const USAGE = `Future ${VERSION} — a tiny language that compiles to JavaScript.
29
+
30
+ Usage:
31
+ future run <file.future> Compile and run a program
32
+ future compile <file.future> Compile to JavaScript (<file>.js)
33
+ future new <name> Create a new project
34
+ future check <file.future> Check for syntax errors
35
+ future fmt <file.future> Format source code in-place
36
+ future playground Launch the interactive playground
37
+ future doctor Check your environment
38
+ future help Show this help
39
+ future --version Show the version
40
+
41
+ Import system:
42
+ use "./utils.future" Import all functions from a file
43
+ use "./math.future" as math Import as a namespace (math.add, math.pi …)
44
+ `;
45
+
46
+ async function main(argv) {
47
+ const [command, arg] = argv;
48
+ switch (command) {
49
+ case 'run': return cmdRun(arg);
50
+ case 'compile': return cmdCompile(arg);
51
+ case 'new': return cmdNew(arg);
52
+ case 'check': return cmdCheck(arg);
53
+ case 'fmt': return cmdFmt(arg);
54
+ case 'playground': return cmdPlayground();
55
+ case 'doctor': return cmdDoctor();
56
+ case 'version': case '--version': case '-v':
57
+ console.log(`future ${VERSION}`); return 0;
58
+ case undefined: case 'help': case '--help': case '-h':
59
+ console.log(USAGE); return 0;
60
+ default:
61
+ process.stderr.write(`Unknown command: ${command}\n\n${USAGE}`); return 1;
62
+ }
63
+ }
64
+
65
+ // ---------------------------------------------------------------------------
66
+ // Helpers
67
+ // ---------------------------------------------------------------------------
68
+
69
+ function readSource(file) {
70
+ if (!file) throw new FutureError('No input file provided');
71
+ if (extname(file) !== '.future') {
72
+ process.stderr.write(`warning: '${file}' does not have a .future extension\n`);
73
+ }
74
+ const path = resolve(file);
75
+ return { path, source: readFileSync(path, 'utf8') };
76
+ }
77
+
78
+ /** Relative `./...` specifier from outDir to the runtime (for compile). */
79
+ function relativeRuntimeSpecifier(outDir) {
80
+ let rel = relative(outDir, RUNTIME_INDEX).split('\\').join('/');
81
+ if (!rel.startsWith('.')) rel = `./${rel}`;
82
+ return rel;
83
+ }
84
+
85
+ function compileOrReport(source, file, options) {
86
+ try {
87
+ return compile(source, options);
88
+ } catch (err) {
89
+ if (err instanceof FutureError) { reportFutureError(err, source, file); return null; }
90
+ throw err;
91
+ }
92
+ }
93
+
94
+ function fail(err, file) {
95
+ if (err instanceof FutureError) {
96
+ process.stderr.write(`error[${err.phase}]: ${err.message}\n`); return 1;
97
+ }
98
+ if (err?.code === 'ENOENT') {
99
+ process.stderr.write(`error: cannot open file '${file}'\n`); return 1;
100
+ }
101
+ throw err;
102
+ }
103
+
104
+ function reportFutureError(err, source, file) {
105
+ const where = err.line != null ? `${file}:${err.line}:${err.column}` : file;
106
+ process.stderr.write(`error[${err.phase}]: ${err.message}\n`);
107
+ process.stderr.write(` --> ${where}\n`);
108
+ if (err.line != null) {
109
+ const srcLine = source.split('\n')[err.line - 1] ?? '';
110
+ const gutter = String(err.line);
111
+ process.stderr.write(` ${gutter} | ${srcLine}\n`);
112
+ const pad = ' '.repeat(gutter.length) + ' ' + ' '.repeat((err.column ?? 1) - 1);
113
+ process.stderr.write(` ${pad}^\n`);
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Find all `use "./..."` paths in a source string by parsing the AST.
119
+ * Returns an array of { path, alias } objects.
120
+ */
121
+ function findUseStatements(source) {
122
+ try {
123
+ const tokens = tokenize(source);
124
+ const ast = parse(tokens);
125
+ return ast.body
126
+ .filter((s) => s.type === 'UseStatement')
127
+ .map((s) => ({ path: s.path, alias: s.alias }));
128
+ } catch {
129
+ return [];
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Recursively compile all .future dependencies to temp .mjs files.
135
+ * Returns a pathMap: Map<originalRelPath, fileURL string>.
136
+ */
137
+ function compileDepsToTemp(sourcePath, sourceText, tempDir, pathMap = new Map()) {
138
+ const uses = findUseStatements(sourceText);
139
+ for (const { path: relPath } of uses) {
140
+ if (pathMap.has(relPath)) continue; // already compiled
141
+ const depAbsPath = resolve(dirname(sourcePath), relPath);
142
+ if (!existsSync(depAbsPath)) continue;
143
+ const depSource = readFileSync(depAbsPath, 'utf8');
144
+
145
+ // Compile dep as a module.
146
+ const depJs = compileDepModule(depSource, depAbsPath, tempDir, pathMap);
147
+ if (depJs === null) return null; // propagate error
148
+
149
+ const depName = basename(relPath, extname(relPath));
150
+ const tmpPath = join(tempDir, `future-dep-${process.pid}-${depName}-${Date.now()}.mjs`);
151
+ writeFileSync(tmpPath, depJs, 'utf8');
152
+ pathMap.set(relPath, pathToFileURL(tmpPath).href);
153
+
154
+ // Recurse into the dep's own imports.
155
+ const sub = compileDepsToTemp(depAbsPath, depSource, tempDir, pathMap);
156
+ if (sub === null) return null;
157
+ }
158
+ return pathMap;
159
+ }
160
+
161
+ function compileDepModule(source, sourcePath, tempDir, pathMap) {
162
+ const uses = findUseStatements(source);
163
+ // Ensure transitive deps are compiled first so pathMap is populated.
164
+ for (const { path: relPath } of uses) {
165
+ if (pathMap.has(relPath)) continue;
166
+ const depAbsPath = resolve(dirname(sourcePath), relPath);
167
+ if (!existsSync(depAbsPath)) continue;
168
+ const depSrc = readFileSync(depAbsPath, 'utf8');
169
+ const sub = compileDepModule(depSrc, depAbsPath, tempDir, pathMap);
170
+ if (sub === null) return null;
171
+ const depName = basename(relPath, extname(relPath));
172
+ const tmpPath = join(tempDir, `future-dep-${process.pid}-${depName}-${Date.now()}.mjs`);
173
+ writeFileSync(tmpPath, sub, 'utf8');
174
+ pathMap.set(relPath, pathToFileURL(tmpPath).href);
175
+ }
176
+
177
+ return compile(source, {
178
+ runtimeSpecifier: pathToFileURL(RUNTIME_INDEX).href,
179
+ isModule: true,
180
+ pathMap,
181
+ resolveSource: (relPath) => {
182
+ const abs = resolve(dirname(sourcePath), relPath);
183
+ return existsSync(abs) ? readFileSync(abs, 'utf8') : null;
184
+ },
185
+ });
186
+ }
187
+
188
+ // ---------------------------------------------------------------------------
189
+ // Commands
190
+ // ---------------------------------------------------------------------------
191
+
192
+ function cmdCompile(file) {
193
+ let path, source;
194
+ try { ({ path, source } = readSource(file)); }
195
+ catch (err) { return fail(err, file); }
196
+
197
+ const outDir = dirname(path);
198
+
199
+ // Compile each imported .future dep as a module next to its source.
200
+ const uses = findUseStatements(source);
201
+ for (const { path: relPath } of uses) {
202
+ const depAbsPath = resolve(outDir, relPath);
203
+ if (!existsSync(depAbsPath)) {
204
+ process.stderr.write(`warning: imported file not found: ${relPath}\n`);
205
+ continue;
206
+ }
207
+ const depSource = readFileSync(depAbsPath, 'utf8');
208
+ const depOutDir = dirname(depAbsPath);
209
+ const depJs = compileOrReport(depSource, relPath, {
210
+ runtimeSpecifier: relativeRuntimeSpecifier(depOutDir),
211
+ isModule: true,
212
+ resolveSource: (p) => {
213
+ const abs = resolve(depOutDir, p);
214
+ return existsSync(abs) ? readFileSync(abs, 'utf8') : null;
215
+ },
216
+ });
217
+ if (depJs === null) return 1;
218
+ const depOut = join(depOutDir, `${basename(depAbsPath, extname(depAbsPath))}.js`);
219
+ writeFileSync(depOut, depJs, 'utf8');
220
+ console.log(`Compiled ${relPath} -> ${depOut}`);
221
+ }
222
+
223
+ const js = compileOrReport(source, file, {
224
+ runtimeSpecifier: relativeRuntimeSpecifier(outDir),
225
+ resolveSource: (relPath) => {
226
+ const abs = resolve(outDir, relPath);
227
+ return existsSync(abs) ? readFileSync(abs, 'utf8') : null;
228
+ },
229
+ });
230
+ if (js === null) return 1;
231
+
232
+ const outPath = join(outDir, `${basename(path, extname(path))}.js`);
233
+ writeFileSync(outPath, js, 'utf8');
234
+ console.log(`Compiled ${file} -> ${outPath}`);
235
+ return 0;
236
+ }
237
+
238
+ async function cmdRun(file) {
239
+ let path, source;
240
+ try { ({ path, source } = readSource(file)); }
241
+ catch (err) { return fail(err, file); }
242
+
243
+ // Compile dependencies to temp .mjs files.
244
+ const tempDir = tmpdir();
245
+ const pathMap = compileDepsToTemp(path, source, tempDir);
246
+ if (pathMap === null) return 1; // error already reported
247
+
248
+ const js = compileOrReport(source, file, {
249
+ runtimeSpecifier: pathToFileURL(RUNTIME_INDEX).href,
250
+ pathMap,
251
+ resolveSource: (relPath) => {
252
+ const abs = resolve(dirname(path), relPath);
253
+ return existsSync(abs) ? readFileSync(abs, 'utf8') : null;
254
+ },
255
+ });
256
+ if (js === null) return 1;
257
+
258
+ const tmp = join(tempDir, `future-${process.pid}-${Date.now()}.mjs`);
259
+ writeFileSync(tmp, js, 'utf8');
260
+ const depTmps = [...pathMap.values()].map((u) => fileURLToPath(u));
261
+ try {
262
+ await import(pathToFileURL(tmp).href);
263
+ return 0;
264
+ } catch (err) {
265
+ process.stderr.write(`runtime error: ${err.message}\n`);
266
+ return 1;
267
+ } finally {
268
+ try { unlinkSync(tmp); } catch { /* ignore */ }
269
+ for (const p of depTmps) { try { unlinkSync(p); } catch { /* ignore */ } }
270
+ }
271
+ }
272
+
273
+ /** Syntax-check only — no output generated. */
274
+ function cmdCheck(file) {
275
+ let path, source;
276
+ try { ({ path, source } = readSource(file)); }
277
+ catch (err) { return fail(err, file); }
278
+
279
+ try {
280
+ const tokens = tokenize(source);
281
+ parse(tokens);
282
+ console.log(`✓ ${file} — no errors`);
283
+ return 0;
284
+ } catch (err) {
285
+ if (err instanceof FutureError) {
286
+ reportFutureError(err, source, file);
287
+ return 1;
288
+ }
289
+ throw err;
290
+ }
291
+ }
292
+
293
+ /** Format a .future file in-place. */
294
+ function cmdFmt(file) {
295
+ let path, source;
296
+ try { ({ path, source } = readSource(file)); }
297
+ catch (err) { return fail(err, file); }
298
+
299
+ // Validate first.
300
+ try {
301
+ parse(tokenize(source));
302
+ } catch (err) {
303
+ if (err instanceof FutureError) {
304
+ reportFutureError(err, source, file);
305
+ process.stderr.write(`fmt: file has errors — not formatted\n`);
306
+ return 1;
307
+ }
308
+ throw err;
309
+ }
310
+
311
+ const formatted = format(source);
312
+ if (formatted === source) {
313
+ console.log(`${file} — already formatted`);
314
+ } else {
315
+ writeFileSync(path, formatted, 'utf8');
316
+ console.log(`${file} — formatted`);
317
+ }
318
+ return 0;
319
+ }
320
+
321
+ /** Create a new project scaffold. */
322
+ function cmdNew(name) {
323
+ if (!name) {
324
+ process.stderr.write('Usage: future new <project-name>\n');
325
+ return 1;
326
+ }
327
+ const dir = resolve(name);
328
+ if (existsSync(dir)) {
329
+ process.stderr.write(`error: directory '${name}' already exists\n`);
330
+ return 1;
331
+ }
332
+ mkdirSync(dir, { recursive: true });
333
+ writeFileSync(join(dir, 'main.future'), `# ${name}\n\nprint "Hello from ${name}!"\n`, 'utf8');
334
+ console.log(`Created project '${name}'/`);
335
+ console.log(` ${name}/main.future`);
336
+ console.log(`\nRun it with: future run ${name}/main.future`);
337
+ return 0;
338
+ }
339
+
340
+ /** Launch the interactive playground. */
341
+ async function cmdPlayground() {
342
+ const serverPath = join(PROJECT_ROOT, 'server.js');
343
+ if (!existsSync(serverPath)) {
344
+ process.stderr.write('error: playground server not found (server.js missing)\n');
345
+ return 1;
346
+ }
347
+ console.log('Starting Future Playground…');
348
+ await import(pathToFileURL(serverPath).href);
349
+ return 0;
350
+ }
351
+
352
+ /** Environment health check. */
353
+ async function cmdDoctor() {
354
+ const check = (ok, label) =>
355
+ console.log(`${ok ? '✓' : '✗'} ${label}`);
356
+
357
+ console.log(`\nDoctor:`);
358
+ console.log(`Future ${VERSION}\n`);
359
+
360
+ // Node.js version.
361
+ const [major] = process.versions.node.split('.').map(Number);
362
+ check(major >= 22, `Node.js ${process.versions.node}`);
363
+
364
+ // Runtime loadable.
365
+ let runtimeOk = false;
366
+ try { await import(pathToFileURL(RUNTIME_INDEX).href); runtimeOk = true; } catch { /* */ }
367
+ check(runtimeOk, 'Runtime OK');
368
+
369
+ // AI provider configured.
370
+ const aiOk = !!(process.env.ANTHROPIC_API_KEY || process.env.OPENAI_API_KEY ||
371
+ process.env.OPENAI_BASE_URL);
372
+ check(aiOk, 'AI Provider Configured');
373
+
374
+ // MQTT optional dep.
375
+ let mqttOk = false;
376
+ try { await import('mqtt'); mqttOk = true; } catch { /* */ }
377
+ check(mqttOk, 'MQTT Available');
378
+
379
+ // Browser build.
380
+ const browserBuild = join(PROJECT_ROOT, 'future-browser.js');
381
+ check(existsSync(browserBuild), 'Browser Build Available');
382
+
383
+ // Examples installed.
384
+ const examplesDir = join(PROJECT_ROOT, 'examples');
385
+ check(existsSync(examplesDir), 'Examples Installed');
386
+
387
+ console.log('');
388
+ return 0;
389
+ }
390
+
391
+ process.exit(await main(process.argv.slice(2)));
package/src/errors.js ADDED
@@ -0,0 +1,21 @@
1
+ // errors.js
2
+ // A single custom error type used across every phase of the Future compiler.
3
+ // Carrying `line`/`column` lets the CLI render helpful, source-aware diagnostics
4
+ // instead of opaque stack traces.
5
+
6
+ export class FutureError extends Error {
7
+ /**
8
+ * @param {string} message Human-readable description of the problem.
9
+ * @param {number | null} [line] 1-based line number where the error occurred.
10
+ * @param {number | null} [column] 1-based column number where the error occurred.
11
+ * @param {string} [phase] Which compiler phase produced the error
12
+ * ('lex' | 'parse' | 'codegen' | 'compile').
13
+ */
14
+ constructor(message, line = null, column = null, phase = 'compile') {
15
+ super(message);
16
+ this.name = 'FutureError';
17
+ this.line = line;
18
+ this.column = column;
19
+ this.phase = phase;
20
+ }
21
+ }
@@ -0,0 +1,48 @@
1
+ // formatter.js
2
+ // Source-level formatter for Future code.
3
+ // Uses a line-based indent tracker — fast and tolerant of partial parses.
4
+
5
+ const INDENT = ' '; // 4 spaces
6
+
7
+ // Regex patterns (match after leading whitespace is stripped).
8
+ const BLOCK_OPEN = /^(if|function|for|while|try|on|every|stream|agent)\b/;
9
+ const BLOCK_CLOSE = /^end\b/;
10
+ const BLOCK_MID = /^(else|catch)\b/;
11
+
12
+ /**
13
+ * Format Future source code.
14
+ * @param {string} source
15
+ * @returns {string} Reformatted source.
16
+ */
17
+ export function format(source) {
18
+ const lines = source.split('\n');
19
+ const result = [];
20
+ let depth = 0;
21
+
22
+ for (const raw of lines) {
23
+ const trimmed = raw.trim();
24
+
25
+ // Preserve blank lines and comments at their natural indent.
26
+ if (trimmed === '') {
27
+ result.push('');
28
+ continue;
29
+ }
30
+ if (trimmed.startsWith('#')) {
31
+ result.push(INDENT.repeat(depth) + trimmed);
32
+ continue;
33
+ }
34
+
35
+ if (BLOCK_CLOSE.test(trimmed)) {
36
+ depth = Math.max(0, depth - 1);
37
+ result.push(INDENT.repeat(depth) + trimmed);
38
+ } else if (BLOCK_MID.test(trimmed)) {
39
+ // `else` / `catch` sit at the same level as the opening keyword.
40
+ result.push(INDENT.repeat(Math.max(0, depth - 1)) + trimmed);
41
+ } else {
42
+ result.push(INDENT.repeat(depth) + trimmed);
43
+ if (BLOCK_OPEN.test(trimmed)) depth++;
44
+ }
45
+ }
46
+
47
+ return result.join('\n').trimEnd() + '\n';
48
+ }