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/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.0';
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 new <name> Create a new project
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 Launch the interactive playground
37
- future doctor Check your environment
38
- future help Show this help
39
- future --version Show the 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 = argv.includes('--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 js = compileOrReport(source, file, {
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 (js === null) return 1;
241
-
242
- const outPath = join(outDir, `${basename(path, extname(path))}.js`);
243
- writeFileSync(outPath, js, 'utf8');
244
- console.log(`Compiled ${file} -> ${outPath}`);
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
- lines.push(this.genStatement(stmt, 0, /* topLevel= */ true));
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 {
@@ -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
+ }