future-lang 0.4.0 → 0.4.2
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 +47 -13
- package/FUTURE_FOR_LLMS.md +55 -2
- package/MIGRATION.md +209 -1
- package/README.md +103 -11
- package/ROADMAP.md +58 -11
- package/package.json +1 -1
- package/runtime/ai.js +54 -9
- package/runtime/assert.js +27 -0
- package/runtime/http.js +54 -8
- package/runtime/index.js +19 -4
- package/runtime/providers/anthropic.js +70 -11
- package/runtime/providers/openai-compat.js +65 -19
- package/src/cli.js +137 -17
- package/src/generator.js +8 -1
- package/src/parser.js +1 -1
- package/src/sourcemap.js +69 -0
package/src/cli.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// future help | --help
|
|
12
12
|
// future version | --version
|
|
13
13
|
|
|
14
|
-
import { readFileSync, writeFileSync, unlinkSync, mkdirSync, existsSync } from 'node:fs';
|
|
14
|
+
import { readFileSync, writeFileSync, unlinkSync, mkdirSync, existsSync, readdirSync, statSync } from 'node:fs';
|
|
15
15
|
import { basename, dirname, extname, join, relative, resolve } from 'node:path';
|
|
16
16
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
17
17
|
import { tmpdir } from 'node:os';
|
|
@@ -20,8 +20,9 @@ import process from 'node:process';
|
|
|
20
20
|
import { compile, tokenize, parse } from './index.js';
|
|
21
21
|
import { format } from './formatter.js';
|
|
22
22
|
import { FutureError } from './errors.js';
|
|
23
|
+
import { buildSourceMap } from './sourcemap.js';
|
|
23
24
|
|
|
24
|
-
const VERSION = '0.4.
|
|
25
|
+
const VERSION = '0.4.2';
|
|
25
26
|
const PROJECT_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
26
27
|
const RUNTIME_INDEX = join(PROJECT_ROOT, 'runtime', 'index.js');
|
|
27
28
|
|
|
@@ -30,13 +31,15 @@ const USAGE = `Future ${VERSION} — a tiny language that compiles to JavaScript
|
|
|
30
31
|
Usage:
|
|
31
32
|
future run <file.future> Compile and run a program
|
|
32
33
|
future compile <file.future> Compile to JavaScript (<file>.js)
|
|
33
|
-
future
|
|
34
|
+
future test [pattern] Run *.test.future files
|
|
35
|
+
future ast <file.future> Print the AST as JSON
|
|
36
|
+
future new <name> Create a new project
|
|
34
37
|
future check <file.future> Check for syntax errors
|
|
35
38
|
future fmt <file.future> Format source code in-place
|
|
36
|
-
future playground
|
|
37
|
-
future doctor
|
|
38
|
-
future help
|
|
39
|
-
future --version
|
|
39
|
+
future playground Launch the interactive playground
|
|
40
|
+
future doctor Check your environment
|
|
41
|
+
future help Show this help
|
|
42
|
+
future --version Show the version
|
|
40
43
|
|
|
41
44
|
Import system:
|
|
42
45
|
use "./utils.future" Import all functions from a file
|
|
@@ -45,16 +48,22 @@ Import system:
|
|
|
45
48
|
|
|
46
49
|
Flags:
|
|
47
50
|
future run --debug <file> Show timing for every namespace call
|
|
51
|
+
future compile --sourcemap <file> Also emit a .js.map source map
|
|
52
|
+
future ast --pretty <file> Indented JSON output
|
|
48
53
|
`;
|
|
49
54
|
|
|
50
55
|
async function main(argv) {
|
|
51
|
-
const debug
|
|
56
|
+
const debug = argv.includes('--debug');
|
|
57
|
+
const sourcemap = argv.includes('--sourcemap');
|
|
58
|
+
const pretty = argv.includes('--pretty');
|
|
52
59
|
if (debug) process.env.FUTURE_DEBUG = '1';
|
|
53
|
-
const rest = argv.filter((a) => a !== '--debug');
|
|
60
|
+
const rest = argv.filter((a) => a !== '--debug' && a !== '--sourcemap' && a !== '--pretty');
|
|
54
61
|
const [command, arg] = rest;
|
|
55
62
|
switch (command) {
|
|
56
63
|
case 'run': return cmdRun(arg);
|
|
57
|
-
case 'compile': return cmdCompile(arg);
|
|
64
|
+
case 'compile': return cmdCompile(arg, { sourcemap });
|
|
65
|
+
case 'test': return cmdTest(arg);
|
|
66
|
+
case 'ast': return cmdAst(arg, { pretty });
|
|
58
67
|
case 'new': return cmdNew(arg);
|
|
59
68
|
case 'check': return cmdCheck(arg);
|
|
60
69
|
case 'fmt': return cmdFmt(arg);
|
|
@@ -198,7 +207,7 @@ function compileDepModule(source, sourcePath, tempDir, pathMap) {
|
|
|
198
207
|
// Commands
|
|
199
208
|
// ---------------------------------------------------------------------------
|
|
200
209
|
|
|
201
|
-
function cmdCompile(file) {
|
|
210
|
+
function cmdCompile(file, { sourcemap = false } = {}) {
|
|
202
211
|
let path, source;
|
|
203
212
|
try { ({ path, source } = readSource(file)); }
|
|
204
213
|
catch (err) { return fail(err, file); }
|
|
@@ -230,18 +239,30 @@ function cmdCompile(file) {
|
|
|
230
239
|
console.log(`Compiled ${relPath} -> ${depOut}`);
|
|
231
240
|
}
|
|
232
241
|
|
|
233
|
-
const
|
|
242
|
+
const rawJs = compileOrReport(source, file, {
|
|
234
243
|
runtimeSpecifier: relativeRuntimeSpecifier(outDir),
|
|
244
|
+
sourceMaps: sourcemap,
|
|
235
245
|
resolveSource: (relPath) => {
|
|
236
246
|
const abs = resolve(outDir, relPath);
|
|
237
247
|
return existsSync(abs) ? readFileSync(abs, 'utf8') : null;
|
|
238
248
|
},
|
|
239
249
|
});
|
|
240
|
-
if (
|
|
241
|
-
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
|
|
250
|
+
if (rawJs === null) return 1;
|
|
251
|
+
|
|
252
|
+
const outBase = join(outDir, basename(path, extname(path)));
|
|
253
|
+
const outPath = `${outBase}.js`;
|
|
254
|
+
|
|
255
|
+
if (sourcemap) {
|
|
256
|
+
const mapFile = `${outBase}.js.map`;
|
|
257
|
+
const { code, map } = buildSourceMap(rawJs, basename(file), source);
|
|
258
|
+
writeFileSync(outPath, `${code}//# sourceMappingURL=${basename(mapFile)}\n`, 'utf8');
|
|
259
|
+
writeFileSync(mapFile, JSON.stringify(map), 'utf8');
|
|
260
|
+
console.log(`Compiled ${file} -> ${outPath}`);
|
|
261
|
+
console.log(`Source map -> ${mapFile}`);
|
|
262
|
+
} else {
|
|
263
|
+
writeFileSync(outPath, rawJs, 'utf8');
|
|
264
|
+
console.log(`Compiled ${file} -> ${outPath}`);
|
|
265
|
+
}
|
|
245
266
|
return 0;
|
|
246
267
|
}
|
|
247
268
|
|
|
@@ -328,6 +349,105 @@ function cmdFmt(file) {
|
|
|
328
349
|
return 0;
|
|
329
350
|
}
|
|
330
351
|
|
|
352
|
+
/** Output the AST of a .future file as JSON. */
|
|
353
|
+
function cmdAst(file, { pretty = false } = {}) {
|
|
354
|
+
let path, source;
|
|
355
|
+
try { ({ path, source } = readSource(file)); }
|
|
356
|
+
catch (err) { return fail(err, file); }
|
|
357
|
+
|
|
358
|
+
try {
|
|
359
|
+
const tokens = tokenize(source);
|
|
360
|
+
const ast = parse(tokens);
|
|
361
|
+
process.stdout.write((pretty ? JSON.stringify(ast, null, 2) : JSON.stringify(ast)) + '\n');
|
|
362
|
+
return 0;
|
|
363
|
+
} catch (err) {
|
|
364
|
+
if (err instanceof FutureError) { reportFutureError(err, source, file); return 1; }
|
|
365
|
+
throw err;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** Recursively collect .future files matching a pattern or default test globs. */
|
|
370
|
+
function findTestFiles(pattern) {
|
|
371
|
+
const cwd = process.cwd();
|
|
372
|
+
const files = [];
|
|
373
|
+
|
|
374
|
+
function walk(dir) {
|
|
375
|
+
let entries;
|
|
376
|
+
try { entries = readdirSync(dir); } catch { return; }
|
|
377
|
+
for (const entry of entries) {
|
|
378
|
+
if (entry === 'node_modules') continue;
|
|
379
|
+
const full = join(dir, entry);
|
|
380
|
+
const st = statSync(full);
|
|
381
|
+
if (st.isDirectory()) { walk(full); continue; }
|
|
382
|
+
if (!entry.endsWith('.future')) continue;
|
|
383
|
+
const rel = relative(cwd, full).split('\\').join('/');
|
|
384
|
+
if (pattern) {
|
|
385
|
+
if (rel.includes(pattern) || entry.includes(pattern)) files.push(full);
|
|
386
|
+
} else {
|
|
387
|
+
if (entry.endsWith('.test.future') || rel.startsWith('test/')) files.push(full);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
walk(cwd);
|
|
393
|
+
return files;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/** Run *.test.future files and report results. */
|
|
397
|
+
async function cmdTest(pattern) {
|
|
398
|
+
const testFiles = findTestFiles(pattern);
|
|
399
|
+
if (testFiles.length === 0) {
|
|
400
|
+
process.stderr.write('No test files found.\n');
|
|
401
|
+
process.stderr.write(' Naming: *.test.future or test/**/*.future\n');
|
|
402
|
+
return 1;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const tempDir = tmpdir();
|
|
406
|
+
let passed = 0;
|
|
407
|
+
let failed = 0;
|
|
408
|
+
|
|
409
|
+
for (const testFile of testFiles) {
|
|
410
|
+
const rel = relative(process.cwd(), testFile).split('\\').join('/');
|
|
411
|
+
let source;
|
|
412
|
+
try { source = readFileSync(testFile, 'utf8'); } catch (err) { process.stderr.write(`error reading ${rel}: ${err.message}\n`); failed++; continue; }
|
|
413
|
+
|
|
414
|
+
// Compile dependencies.
|
|
415
|
+
const pathMap = compileDepsToTemp(testFile, source, tempDir);
|
|
416
|
+
if (pathMap === null) { failed++; continue; }
|
|
417
|
+
|
|
418
|
+
const js = compileOrReport(source, rel, {
|
|
419
|
+
runtimeSpecifier: pathToFileURL(RUNTIME_INDEX).href,
|
|
420
|
+
pathMap,
|
|
421
|
+
resolveSource: (p) => {
|
|
422
|
+
const abs = resolve(dirname(testFile), p);
|
|
423
|
+
return existsSync(abs) ? readFileSync(abs, 'utf8') : null;
|
|
424
|
+
},
|
|
425
|
+
});
|
|
426
|
+
if (js === null) { failed++; continue; }
|
|
427
|
+
|
|
428
|
+
const tmp = join(tempDir, `future-test-${process.pid}-${Date.now()}.mjs`);
|
|
429
|
+
writeFileSync(tmp, js, 'utf8');
|
|
430
|
+
const depTmps = [...pathMap.values()].map((u) => fileURLToPath(u));
|
|
431
|
+
try {
|
|
432
|
+
await import(pathToFileURL(tmp).href);
|
|
433
|
+
console.log(` ✓ ${rel}`);
|
|
434
|
+
passed++;
|
|
435
|
+
} catch (err) {
|
|
436
|
+
const isAssert = err.name === 'AssertionError' || err.namespace === 'assert';
|
|
437
|
+
process.stderr.write(` ✗ ${rel}\n`);
|
|
438
|
+
process.stderr.write(` ${isAssert ? 'AssertionError' : err.name ?? 'Error'}: ${err.message}\n`);
|
|
439
|
+
failed++;
|
|
440
|
+
} finally {
|
|
441
|
+
try { unlinkSync(tmp); } catch { /* ignore */ }
|
|
442
|
+
for (const p of depTmps) { try { unlinkSync(p); } catch { /* ignore */ } }
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const total = passed + failed;
|
|
447
|
+
console.log(`\n${passed}/${total} tests passed${failed > 0 ? `, ${failed} failed` : ''}`);
|
|
448
|
+
return failed > 0 ? 1 : 0;
|
|
449
|
+
}
|
|
450
|
+
|
|
331
451
|
/** Create a new project scaffold. */
|
|
332
452
|
function cmdNew(name) {
|
|
333
453
|
if (!name) {
|
package/src/generator.js
CHANGED
|
@@ -24,6 +24,7 @@ export const NAMESPACES = new Set([
|
|
|
24
24
|
'rag', 'vision', 'home', // AI / automation extension points
|
|
25
25
|
'memory', 'schedule', 'system', 'device', // optional new modules
|
|
26
26
|
'math', // general-purpose math
|
|
27
|
+
'assert', // test assertions
|
|
27
28
|
]);
|
|
28
29
|
|
|
29
30
|
export class Generator {
|
|
@@ -36,6 +37,7 @@ export class Generator {
|
|
|
36
37
|
this.runtimeSpecifier = options.runtimeSpecifier ?? 'future-lang/runtime';
|
|
37
38
|
this.browserMode = options.browserMode ?? false;
|
|
38
39
|
this.isModule = options.isModule ?? false;
|
|
40
|
+
this.sourceMaps = options.sourceMaps ?? false;
|
|
39
41
|
// Map<importedFuturePath, string[]> — exported names for non-aliased use statements.
|
|
40
42
|
this.importedNames = options.importedNames ?? new Map();
|
|
41
43
|
// Map<importedFuturePath, resolvedJsPath> — path override for `future run` temp files.
|
|
@@ -79,7 +81,12 @@ export class Generator {
|
|
|
79
81
|
}
|
|
80
82
|
for (const stmt of program.body) {
|
|
81
83
|
if (stmt.type === NodeType.UseStatement) continue; // already emitted above
|
|
82
|
-
|
|
84
|
+
const code = this.genStatement(stmt, 0, /* topLevel= */ true);
|
|
85
|
+
if (this.sourceMaps && stmt.line != null) {
|
|
86
|
+
lines.push(`/*@FL:${stmt.line}*/${code}`);
|
|
87
|
+
} else {
|
|
88
|
+
lines.push(code);
|
|
89
|
+
}
|
|
83
90
|
}
|
|
84
91
|
return lines.join('\n') + '\n';
|
|
85
92
|
}
|
package/src/parser.js
CHANGED
|
@@ -19,7 +19,7 @@ const EXPR_TERMINATORS = new Set(['END', 'ELSE', 'CATCH', 'EOF']);
|
|
|
19
19
|
/** Built-in namespace names that cannot be redefined by user code. */
|
|
20
20
|
const RESERVED_NAMESPACES = new Set([
|
|
21
21
|
'ai', 'http', 'mqtt', 'tts', 'rag', 'vision', 'home',
|
|
22
|
-
'memory', 'schedule', 'system', 'device', 'math',
|
|
22
|
+
'memory', 'schedule', 'system', 'device', 'math', 'assert',
|
|
23
23
|
]);
|
|
24
24
|
|
|
25
25
|
export class Parser {
|
package/src/sourcemap.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// src/sourcemap.js — Source map generation (Source Map v3).
|
|
2
|
+
// The generator embeds @FL:N markers (inside block comments) at statement lines.
|
|
3
|
+
// This module strips them and produces a v3 source map + clean JS.
|
|
4
|
+
|
|
5
|
+
const B64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
6
|
+
|
|
7
|
+
function encodeVlq(value) {
|
|
8
|
+
let vlq = value < 0 ? ((-value) << 1) | 1 : (value << 1);
|
|
9
|
+
let out = '';
|
|
10
|
+
do {
|
|
11
|
+
let digit = vlq & 0x1F;
|
|
12
|
+
vlq >>>= 5;
|
|
13
|
+
if (vlq > 0) digit |= 0x20;
|
|
14
|
+
out += B64[digit];
|
|
15
|
+
} while (vlq > 0);
|
|
16
|
+
return out;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const MARKER_RE = /^\/\*@FL:(\d+)\*\//;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Strip @FL:N markers from generated JS and build a v3 source map.
|
|
23
|
+
*
|
|
24
|
+
* @param {string} js Generated JS (possibly with @FL markers)
|
|
25
|
+
* @param {string} sourceFile Original .future filename (for `sources` field)
|
|
26
|
+
* @param {string} futureSource Original .future source text (for `sourcesContent`)
|
|
27
|
+
* @returns {{ code: string, map: object }}
|
|
28
|
+
*/
|
|
29
|
+
export function buildSourceMap(js, sourceFile, futureSource) {
|
|
30
|
+
const jsLines = js.split('\n');
|
|
31
|
+
const cleanLines = [];
|
|
32
|
+
const mappings = [];
|
|
33
|
+
|
|
34
|
+
// Delta state for VLQ.
|
|
35
|
+
let prevSrcLine = 0;
|
|
36
|
+
let prevSrcCol = 0;
|
|
37
|
+
|
|
38
|
+
for (const line of jsLines) {
|
|
39
|
+
const m = MARKER_RE.exec(line);
|
|
40
|
+
if (m) {
|
|
41
|
+
const srcLine = parseInt(m[1], 10) - 1; // 0-indexed
|
|
42
|
+
const srcCol = 0;
|
|
43
|
+
// Segment: [genCol=0, sourceIdx=0, srcLine delta, srcCol delta]
|
|
44
|
+
const seg = encodeVlq(0)
|
|
45
|
+
+ encodeVlq(0)
|
|
46
|
+
+ encodeVlq(srcLine - prevSrcLine)
|
|
47
|
+
+ encodeVlq(srcCol - prevSrcCol);
|
|
48
|
+
mappings.push(seg);
|
|
49
|
+
prevSrcLine = srcLine;
|
|
50
|
+
prevSrcCol = srcCol;
|
|
51
|
+
cleanLines.push(line.slice(m[0].length));
|
|
52
|
+
} else {
|
|
53
|
+
// No marker — emit an empty mapping for this line.
|
|
54
|
+
mappings.push('');
|
|
55
|
+
cleanLines.push(line);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const map = {
|
|
60
|
+
version: 3,
|
|
61
|
+
file: sourceFile.replace(/\.future$/, '.js'),
|
|
62
|
+
sources: [sourceFile],
|
|
63
|
+
sourcesContent: [futureSource],
|
|
64
|
+
names: [],
|
|
65
|
+
mappings: mappings.join(';'),
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
return { code: cleanLines.join('\n'), map };
|
|
69
|
+
}
|