@hatchingpoint/point 0.0.3 → 0.0.6

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/core/cli.ts CHANGED
@@ -1,14 +1,19 @@
1
1
  import { dirname, resolve } from "node:path";
2
+ import { tmpdir } from "node:os";
3
+ import type { PointCoreDeclaration, PointCoreProgram } from "./ast.ts";
2
4
  import { checkPointCore } from "./check.ts";
3
5
  import { createPointCoreIndex, createPointCoreRepairPlan, explainPointCoreRef } from "./context.ts";
6
+ import { createSemanticIndex, explainSemanticRef, mapPublicDiagnostics } from "../semantic/context.ts";
4
7
  import { emitPointCoreTypeScript } from "./emit-typescript.ts";
5
- import { formatPointCore } from "./format.ts";
6
- import { parsePointCore } from "./parser.ts";
8
+ import { emitPointCoreJavaScript } from "./emit-javascript.ts";
9
+ import { formatPointSource } from "./format.ts";
10
+ import { isCacheHit, isIncrementalEnabled, readBuildCache, recordCacheEntry, writeBuildCache } from "./incremental.ts";
11
+ import { parsePointSource } from "./parser.ts";
7
12
 
8
13
  const DEFAULT_INPUT = "examples/math.point";
9
14
  const DEFAULT_OUTPUT = "generated/math.ast.json";
10
15
  const DEFAULT_TS_OUTPUT = "generated/math.ts";
11
- const DEFAULT_PATTERN = "examples/**/*.point";
16
+ const DEFAULT_PATTERNS = ["examples/**/*.point", "std/**/*.point", "compiler/**/*.point"];
12
17
  const GENERATED_DIR = "generated";
13
18
 
14
19
  export async function main() {
@@ -18,24 +23,28 @@ export async function main() {
18
23
  return;
19
24
  }
20
25
 
26
+ if (command === "repl") {
27
+ await runRepl(Bun.argv.slice(3).join(" "));
28
+ return;
29
+ }
30
+
21
31
  const inputPath = resolve(process.cwd(), input);
22
32
  const source = await Bun.file(inputPath).text();
23
- const program = parsePointCore(source);
33
+ const program = parsePointSource(source);
24
34
  const diagnostics = checkPointCore(program);
25
35
 
26
36
  if (command === "fmt") {
27
- await Bun.write(inputPath, formatPointCore(program));
28
- console.log(`Point core fmt wrote ${input}`);
37
+ await Bun.write(inputPath, formatPointSource(source));
38
+ console.log(`Point fmt wrote ${input}`);
29
39
  return;
30
40
  }
31
41
 
32
42
  if (command === "fmt-check") {
33
- const formatted = formatPointCore(program);
34
- if (source !== formatted) {
35
- console.error(`Point core fmt check failed: ${input}`);
43
+ if (source !== formatPointSource(source)) {
44
+ console.error(`Point fmt check failed: ${input}`);
36
45
  process.exit(1);
37
46
  }
38
- console.log(`Point core fmt check passed: ${input}`);
47
+ console.log(`Point fmt check passed: ${input}`);
39
48
  return;
40
49
  }
41
50
 
@@ -49,24 +58,34 @@ export async function main() {
49
58
  }
50
59
 
51
60
  if (command === "check-json") {
52
- console.log(JSON.stringify({ schemaVersion: "point.core.check.v1", ok: diagnostics.length === 0, diagnostics }, null, 2));
61
+ const outputDiagnostics = mapPublicDiagnostics(program, diagnostics);
62
+ console.log(JSON.stringify({ schemaVersion: "point.core.check.v1", ok: diagnostics.length === 0, diagnostics: outputDiagnostics }, null, 2));
53
63
  if (diagnostics.length > 0) process.exit(1);
54
64
  return;
55
65
  }
56
66
 
57
67
  if (command === "index") {
68
+ if (program.semanticSource) {
69
+ console.log(JSON.stringify(createSemanticIndex(program.semanticSource), null, 2));
70
+ return;
71
+ }
58
72
  console.log(JSON.stringify(createPointCoreIndex(program), null, 2));
59
73
  return;
60
74
  }
61
75
 
62
76
  if (command === "explain") {
63
77
  const ref = output;
78
+ if (program.semanticSource && ref.startsWith("point://semantic/")) {
79
+ console.log(JSON.stringify(explainSemanticRef(program.semanticSource, ref), null, 2));
80
+ return;
81
+ }
64
82
  console.log(JSON.stringify(explainPointCoreRef(program, ref), null, 2));
65
83
  return;
66
84
  }
67
85
 
68
86
  if (command === "repair-plan") {
69
- console.log(JSON.stringify(createPointCoreRepairPlan(diagnostics), null, 2));
87
+ const outputDiagnostics = mapPublicDiagnostics(program, diagnostics);
88
+ console.log(JSON.stringify(createPointCoreRepairPlan(outputDiagnostics), null, 2));
70
89
  if (diagnostics.length > 0) process.exit(1);
71
90
  return;
72
91
  }
@@ -100,26 +119,70 @@ export async function main() {
100
119
  return;
101
120
  }
102
121
 
122
+ if (command === "build-js") {
123
+ if (diagnostics.length > 0) {
124
+ console.error(JSON.stringify({ ok: false, diagnostics }, null, 2));
125
+ process.exit(1);
126
+ }
127
+ const outputPath = resolve(process.cwd(), output === DEFAULT_OUTPUT ? DEFAULT_TS_OUTPUT.replace(/\.ts$/, ".js") : output);
128
+ await Bun.$`mkdir -p ${dirname(outputPath)}`.quiet();
129
+ await Bun.write(outputPath, emitPointCoreJavaScript(program));
130
+ console.log(`Point core JavaScript build wrote ${outputPath.replaceAll("\\", "/")}`);
131
+ return;
132
+ }
133
+
134
+ if (command === "run") {
135
+ if (diagnostics.length > 0) {
136
+ console.error(JSON.stringify({ ok: false, diagnostics }, null, 2));
137
+ process.exit(1);
138
+ }
139
+ const runOutput = resolve(tmpdir(), `point-run-${Date.now()}.ts`);
140
+ await Bun.write(runOutput, emitPointCoreTypeScript(program));
141
+ let entryName: string | null = null;
142
+ try {
143
+ const mod = await import(pathToFileUrl(runOutput));
144
+ entryName = findRunEntryName(program);
145
+ if (!entryName) throw new Error("No zero-argument entrypoint found. Define an action or calculation with no inputs.");
146
+ const entry = mod[entryName];
147
+ if (typeof entry !== "function") throw new Error(`Entrypoint ${entryName} was not exported.`);
148
+ const value = await entry();
149
+ if (value !== undefined) console.log(typeof value === "string" ? value : JSON.stringify(value));
150
+ } catch (error) {
151
+ console.error(`Runtime error in ${runtimeSourceLocation(program, input, entryName)}: ${error instanceof Error ? error.message : String(error)}`);
152
+ process.exit(1);
153
+ }
154
+ return;
155
+ }
156
+
157
+ if (command === "test") {
158
+ if (diagnostics.length > 0) {
159
+ console.error(JSON.stringify({ ok: false, diagnostics }, null, 2));
160
+ process.exit(1);
161
+ }
162
+ const result = await runPointTests(program, input);
163
+ console.log(JSON.stringify(result, null, 2));
164
+ if (!result.ok) process.exit(1);
165
+ return;
166
+ }
167
+
103
168
  throw new Error(`Unknown point core command: ${command}`);
104
169
  }
105
170
 
106
171
  async function runProjectCommand(command: string) {
107
172
  const inputs = await discoverInputs();
108
- if (inputs.length === 0) throw new Error(`No Point core files matched ${DEFAULT_PATTERN}`);
173
+ if (inputs.length === 0) throw new Error(`No Point core files matched ${DEFAULT_PATTERNS.join(", ")}`);
109
174
  const results = await Promise.all(inputs.map((input) => loadCoreFile(input)));
175
+ const graph = createModuleGraph(results);
176
+ const orderedResults = orderByDependencies(results, graph);
110
177
 
111
178
  if (command === "fmt-all") {
112
- await Promise.all(
113
- results.map((result) =>
114
- Bun.write(resolve(process.cwd(), result.input), formatPointCore(result.program)),
115
- ),
116
- );
117
- console.log(`Point core fmt wrote ${results.length} files`);
179
+ await Promise.all(results.map((result) => Bun.write(resolve(process.cwd(), result.input), formatPointSource(result.source))));
180
+ console.log(`Point fmt wrote ${results.length} files`);
118
181
  return;
119
182
  }
120
183
 
121
184
  if (command === "fmt-check-all") {
122
- const unformatted = results.filter((result) => result.source !== formatPointCore(result.program));
185
+ const unformatted = results.filter((result) => result.source !== formatPointSource(result.source));
123
186
  if (unformatted.length > 0) {
124
187
  console.error(JSON.stringify({ ok: false, unformatted: unformatted.map((result) => result.input) }, null, 2));
125
188
  process.exit(1);
@@ -129,26 +192,40 @@ async function runProjectCommand(command: string) {
129
192
  }
130
193
 
131
194
  if (command === "check-all") {
132
- const diagnostics = results.flatMap((result) =>
133
- checkPointCore(result.program).map((diagnostic) => ({ ...diagnostic, file: result.input })),
134
- );
195
+ const cache = isIncrementalEnabled() ? await readBuildCache() : null;
196
+ let manifest = cache ?? { schemaVersion: "point.cache.v1" as const, entries: {} };
197
+ const diagnostics = [];
198
+ let skipped = 0;
199
+ for (const result of orderedResults) {
200
+ if (cache && isCacheHit(manifest, result.input, result.source)) {
201
+ skipped += 1;
202
+ continue;
203
+ }
204
+ const fileDiagnostics = checkPointCore(programWithDependencyDeclarations(result, graph)).map((diagnostic) => ({
205
+ ...diagnostic,
206
+ file: result.input,
207
+ }));
208
+ diagnostics.push(...fileDiagnostics);
209
+ if (cache) manifest = recordCacheEntry(manifest, result.input, result.source, fileDiagnostics.length === 0);
210
+ }
211
+ if (cache) await writeBuildCache(manifest);
135
212
  if (diagnostics.length > 0) {
136
213
  console.error(JSON.stringify({ ok: false, diagnostics }, null, 2));
137
214
  process.exit(1);
138
215
  }
139
- console.log(`Point core check passed: ${results.length} files`);
216
+ console.log(`Point core check passed: ${results.length} files${skipped ? ` (${skipped} cached)` : ""}`);
140
217
  return;
141
218
  }
142
219
 
143
220
  if (command === "build-all") {
144
- const diagnostics = results.flatMap((result) =>
145
- checkPointCore(result.program).map((diagnostic) => ({ ...diagnostic, file: result.input })),
221
+ const diagnostics = orderedResults.flatMap((result) =>
222
+ checkPointCore(programWithDependencyDeclarations(result, graph)).map((diagnostic) => ({ ...diagnostic, file: result.input })),
146
223
  );
147
224
  if (diagnostics.length > 0) {
148
225
  console.error(JSON.stringify({ ok: false, diagnostics }, null, 2));
149
226
  process.exit(1);
150
227
  }
151
- for (const result of results) {
228
+ for (const result of orderedResults) {
152
229
  const output = outputFor(result.input);
153
230
  const outputPath = resolve(process.cwd(), output);
154
231
  await Bun.$`mkdir -p ${dirname(outputPath)}`.quiet();
@@ -159,46 +236,262 @@ async function runProjectCommand(command: string) {
159
236
  }
160
237
 
161
238
  if (command === "build-ts-all") {
162
- const diagnostics = results.flatMap((result) =>
163
- checkPointCore(result.program).map((diagnostic) => ({ ...diagnostic, file: result.input })),
239
+ const diagnostics = orderedResults.flatMap((result) =>
240
+ checkPointCore(programWithDependencyDeclarations(result, graph)).map((diagnostic) => ({ ...diagnostic, file: result.input })),
164
241
  );
165
242
  if (diagnostics.length > 0) {
166
243
  console.error(JSON.stringify({ ok: false, diagnostics }, null, 2));
167
244
  process.exit(1);
168
245
  }
169
- for (const result of results) {
246
+ for (const result of orderedResults) {
170
247
  const output = tsOutputFor(result.input);
171
248
  const outputPath = resolve(process.cwd(), output);
172
249
  await Bun.$`mkdir -p ${dirname(outputPath)}`.quiet();
173
- await Bun.write(outputPath, emitPointCoreTypeScript(result.program));
250
+ await Bun.write(outputPath, emitPointCoreTypeScript(programWithTypeScriptImports(result, graph)));
174
251
  }
175
252
  console.log(`Point core TypeScript build wrote ${results.length} files`);
176
253
  return;
177
254
  }
178
255
 
256
+ if (command === "build-js-all") {
257
+ const diagnostics = orderedResults.flatMap((result) =>
258
+ checkPointCore(programWithDependencyDeclarations(result, graph)).map((diagnostic) => ({ ...diagnostic, file: result.input })),
259
+ );
260
+ if (diagnostics.length > 0) {
261
+ console.error(JSON.stringify({ ok: false, diagnostics }, null, 2));
262
+ process.exit(1);
263
+ }
264
+ for (const result of orderedResults) {
265
+ const output = jsOutputFor(result.input);
266
+ const outputPath = resolve(process.cwd(), output);
267
+ await Bun.$`mkdir -p ${dirname(outputPath)}`.quiet();
268
+ await Bun.write(outputPath, emitPointCoreJavaScript(programWithTypeScriptImports(result, graph)));
269
+ }
270
+ console.log(`Point core JavaScript build wrote ${results.length} files`);
271
+ return;
272
+ }
273
+
274
+ if (command === "test-all") {
275
+ const diagnostics = orderedResults.flatMap((result) =>
276
+ checkPointCore(programWithDependencyDeclarations(result, graph)).map((diagnostic) => ({ ...diagnostic, file: result.input })),
277
+ );
278
+ if (diagnostics.length > 0) {
279
+ console.error(JSON.stringify({ ok: false, diagnostics }, null, 2));
280
+ process.exit(1);
281
+ }
282
+ const results = await Promise.all(orderedResults.map((result) => runPointTests(programWithTypeScriptImports(result, graph), result.input)));
283
+ const failed = results.filter((result) => !result.ok);
284
+ if (failed.length > 0) {
285
+ console.error(JSON.stringify({ ok: false, files: failed }, null, 2));
286
+ process.exit(1);
287
+ }
288
+ console.log(`Point tests passed: ${results.reduce((total, result) => total + result.tests.length, 0)} tests`);
289
+ return;
290
+ }
291
+
179
292
  throw new Error(`Unknown point core command: ${command}`);
180
293
  }
181
294
 
182
295
  async function discoverInputs(): Promise<string[]> {
183
- const glob = new Bun.Glob(DEFAULT_PATTERN);
184
- const inputs: string[] = [];
185
- for await (const input of glob.scan({ cwd: process.cwd(), onlyFiles: true })) {
186
- if (!input.includes("/generated/")) inputs.push(input.replaceAll("\\", "/"));
296
+ const inputs = new Set<string>();
297
+ for (const pattern of DEFAULT_PATTERNS) {
298
+ const glob = new Bun.Glob(pattern);
299
+ for await (const input of glob.scan({ cwd: process.cwd(), onlyFiles: true })) {
300
+ if (!input.includes("/generated/")) inputs.add(input.replaceAll("\\", "/"));
301
+ }
187
302
  }
188
- return inputs.sort((a, b) => a.localeCompare(b));
303
+ return [...inputs].sort((a, b) => a.localeCompare(b));
304
+ }
305
+
306
+ function runtimeSourceLocation(program: PointCoreProgram, input: string, entryName: string | null): string {
307
+ const declaration = program.declarations.find((candidate) => candidate.kind === "function" && candidate.name === entryName);
308
+ const line = declaration?.span?.start.line;
309
+ return line ? `${input}:${line}` : input;
310
+ }
311
+
312
+ async function runRepl(inlineSource: string) {
313
+ const source = inlineSource ? inlineSource.replaceAll("\\n", "\n") : await Bun.stdin.text();
314
+ for (const rawLine of source.split(/\r?\n/)) {
315
+ const line = rawLine.trim();
316
+ if (!line) continue;
317
+ if (line === ".exit" || line === "exit") return;
318
+ try {
319
+ const value = Function(`"use strict"; return (${line.replace(/\band\b/g, "&&").replace(/\bor\b/g, "||")});`)();
320
+ console.log(`${formatReplValue(value)}: ${pointTypeOfRuntimeValue(value)}`);
321
+ } catch (error) {
322
+ console.error(`REPL error: ${error instanceof Error ? error.message : String(error)}`);
323
+ }
324
+ }
325
+ }
326
+
327
+ function formatReplValue(value: unknown): string {
328
+ return typeof value === "string" ? value : JSON.stringify(value);
329
+ }
330
+
331
+ function pointTypeOfRuntimeValue(value: unknown): string {
332
+ if (typeof value === "string") return "Text";
333
+ if (typeof value === "boolean") return "Bool";
334
+ if (typeof value === "number") return Number.isInteger(value) ? "Int" : "Float";
335
+ if (value === null || value === undefined) return "Void";
336
+ if (Array.isArray(value)) return "List";
337
+ return "Record";
338
+ }
339
+
340
+ interface PointTestResult {
341
+ file: string;
342
+ ok: boolean;
343
+ tests: Array<{ name: string; ok: boolean; error?: string }>;
344
+ }
345
+
346
+ async function runPointTests(program: PointCoreProgram, input: string): Promise<PointTestResult> {
347
+ const tests = program.declarations.filter(
348
+ (declaration) =>
349
+ declaration.kind === "function" &&
350
+ declaration.params.length === 0 &&
351
+ declaration.returnType.name === "Bool" &&
352
+ (declaration.semantic?.name.startsWith("test") || declaration.name.startsWith("test")),
353
+ );
354
+ if (tests.length === 0) return { file: input, ok: true, tests: [] };
355
+ const testOutput = resolve(tmpdir(), `point-test-${Date.now()}-${Math.random().toString(16).slice(2)}.ts`);
356
+ await Bun.write(testOutput, emitPointCoreTypeScript(program));
357
+ const mod = await import(pathToFileUrl(testOutput));
358
+ const results = [];
359
+ for (const test of tests) {
360
+ try {
361
+ const candidate = mod[test.name];
362
+ if (typeof candidate !== "function") throw new Error(`Test ${test.name} was not exported.`);
363
+ const value = await candidate();
364
+ results.push({ name: test.semantic?.name ?? test.name, ok: value === true, error: value === true ? undefined : "Expected true." });
365
+ } catch (error) {
366
+ results.push({ name: test.semantic?.name ?? test.name, ok: false, error: error instanceof Error ? error.message : String(error) });
367
+ }
368
+ }
369
+ return { file: input, ok: results.every((result) => result.ok), tests: results };
370
+ }
371
+
372
+ function pathToFileUrl(path: string): string {
373
+ return `file://${path.replaceAll("\\", "/")}`;
374
+ }
375
+
376
+ export function findRunEntryName(program: PointCoreProgram): string | null {
377
+ const zeroArgFunctions = program.declarations.filter((declaration) => declaration.kind === "function" && declaration.params.length === 0);
378
+ const preferred =
379
+ zeroArgFunctions.find((declaration) => declaration.semantic?.kind === "command") ??
380
+ zeroArgFunctions.find((declaration) => declaration.name === "main") ??
381
+ zeroArgFunctions[0];
382
+ return preferred?.name ?? null;
189
383
  }
190
384
 
191
385
  async function loadCoreFile(input: string) {
192
386
  const source = await Bun.file(resolve(process.cwd(), input)).text();
193
- return { input, source, program: parsePointCore(source) };
387
+ return { input, source, program: parsePointSource(source), uses: parseUseDeclarations(source, input) };
388
+ }
389
+
390
+ type CoreFile = Awaited<ReturnType<typeof loadCoreFile>>;
391
+ type ModuleGraph = Map<string, { result: CoreFile; dependencies: CoreFile[] }>;
392
+
393
+ interface UseDeclaration {
394
+ moduleName: string;
395
+ from: string;
396
+ input: string;
397
+ }
398
+
399
+ function parseUseDeclarations(source: string, input: string): UseDeclaration[] {
400
+ return source
401
+ .split(/\r?\n/)
402
+ .map((line) => line.trim().match(/^use\s+([A-Za-z][A-Za-z0-9]*(?:\.[A-Za-z][A-Za-z0-9]*)*)(?:\s+from\s+"([^"]+)")?$/))
403
+ .filter((match): match is RegExpMatchArray => Boolean(match))
404
+ .map((match) => ({ moduleName: match[1]!, from: match[2] ?? stdPathFor(match[1]!), input }));
405
+ }
406
+
407
+ function createModuleGraph(results: CoreFile[]): ModuleGraph {
408
+ const byInput = new Map(results.map((result) => [normalizeInput(result.input), result]));
409
+ const graph: ModuleGraph = new Map();
410
+ for (const result of results) {
411
+ const dependencies = result.uses.map((use) => {
412
+ const resolved = normalizeInput(resolveDependencyInput(use.input, use.from));
413
+ const dependency = byInput.get(resolved);
414
+ if (!dependency) throw new Error(`Cannot resolve Point module ${use.moduleName} from ${use.from} in ${use.input}`);
415
+ return dependency;
416
+ });
417
+ graph.set(normalizeInput(result.input), { result, dependencies });
418
+ }
419
+ return graph;
420
+ }
421
+
422
+ function orderByDependencies(results: CoreFile[], graph: ModuleGraph): CoreFile[] {
423
+ const ordered: CoreFile[] = [];
424
+ const visiting = new Set<string>();
425
+ const visited = new Set<string>();
426
+ const visit = (result: CoreFile) => {
427
+ const key = normalizeInput(result.input);
428
+ if (visited.has(key)) return;
429
+ if (visiting.has(key)) throw new Error(`Cyclic Point module dependency involving ${result.input}`);
430
+ visiting.add(key);
431
+ for (const dependency of graph.get(key)?.dependencies ?? []) visit(dependency);
432
+ visiting.delete(key);
433
+ visited.add(key);
434
+ ordered.push(result);
435
+ };
436
+ for (const result of results) visit(result);
437
+ return ordered;
438
+ }
439
+
440
+ function programWithDependencyDeclarations(result: CoreFile, graph: ModuleGraph): PointCoreProgram {
441
+ const dependencies = graph.get(normalizeInput(result.input))?.dependencies ?? [];
442
+ return {
443
+ ...result.program,
444
+ declarations: [...dependencies.flatMap((dependency) => publicDeclarations(dependency.program)), ...result.program.declarations],
445
+ };
446
+ }
447
+
448
+ function programWithTypeScriptImports(result: CoreFile, graph: ModuleGraph): PointCoreProgram {
449
+ const dependencies = graph.get(normalizeInput(result.input))?.dependencies ?? [];
450
+ const imports: PointCoreDeclaration[] = dependencies.map((dependency) => ({
451
+ kind: "import",
452
+ names: publicDeclarations(dependency.program).map((declaration) => declaration.name).filter(Boolean),
453
+ from: `./${outputBaseName(dependency.input)}`,
454
+ }));
455
+ return { ...result.program, declarations: [...imports.filter((declaration) => declaration.kind !== "import" || declaration.names.length > 0), ...result.program.declarations] };
456
+ }
457
+
458
+ function publicDeclarations(program: PointCoreProgram): Array<Extract<PointCoreDeclaration, { kind: "type" | "function" | "value" | "external" }>> {
459
+ return program.declarations.filter(
460
+ (declaration): declaration is Extract<PointCoreDeclaration, { kind: "type" | "function" | "value" | "external" }> =>
461
+ declaration.kind === "type" || declaration.kind === "function" || declaration.kind === "value" || declaration.kind === "external",
462
+ );
463
+ }
464
+
465
+ function resolveDependencyInput(input: string, from: string): string {
466
+ if (from.startsWith("std/")) return from;
467
+ const base = dirname(resolve(process.cwd(), input));
468
+ return resolve(base, from).replace(resolve(process.cwd()), "").replace(/^[/\\]/, "");
469
+ }
470
+
471
+ function stdPathFor(moduleName: string): string {
472
+ if (!moduleName.startsWith("std.")) throw new Error(`Use declarations without from must target std modules: ${moduleName}`);
473
+ return `${moduleName.replace(/^std\./, "std/").replaceAll(".", "/")}.point`;
474
+ }
475
+
476
+ function normalizeInput(input: string): string {
477
+ return input.replaceAll("\\", "/");
194
478
  }
195
479
 
196
480
  function outputFor(input: string): string {
197
- const name = input.split("/").pop()?.replace(/\.point$/, "") ?? "program";
481
+ const name = outputBaseName(input);
198
482
  return `${GENERATED_DIR}/${name}.ast.json`;
199
483
  }
200
484
 
201
485
  function tsOutputFor(input: string): string {
202
- const name = input.split("/").pop()?.replace(/\.point$/, "") ?? "program";
486
+ const name = outputBaseName(input);
203
487
  return `${GENERATED_DIR}/${name}.ts`;
204
488
  }
489
+
490
+ function jsOutputFor(input: string): string {
491
+ const name = outputBaseName(input);
492
+ return `${GENERATED_DIR}/${name}.js`;
493
+ }
494
+
495
+ function outputBaseName(input: string): string {
496
+ return normalizeInput(input).split("/").pop()?.replace(/\.point$/, "") ?? "program";
497
+ }