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/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
|
+
}
|
package/src/formatter.js
ADDED
|
@@ -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
|
+
}
|