@hatchingpoint/point 0.0.5 → 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 { isSemanticPointSyntax, 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,32 +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
- if (isSemanticPointSyntax(source)) {
28
- console.log(`Point core fmt preserved semantic source: ${input}`);
29
- return;
30
- }
31
- await Bun.write(inputPath, formatPointCore(program));
32
- console.log(`Point core fmt wrote ${input}`);
37
+ await Bun.write(inputPath, formatPointSource(source));
38
+ console.log(`Point fmt wrote ${input}`);
33
39
  return;
34
40
  }
35
41
 
36
42
  if (command === "fmt-check") {
37
- if (isSemanticPointSyntax(source)) {
38
- console.log(`Point core fmt check passed: ${input}`);
39
- return;
40
- }
41
- const formatted = formatPointCore(program);
42
- if (source !== formatted) {
43
- console.error(`Point core fmt check failed: ${input}`);
43
+ if (source !== formatPointSource(source)) {
44
+ console.error(`Point fmt check failed: ${input}`);
44
45
  process.exit(1);
45
46
  }
46
- console.log(`Point core fmt check passed: ${input}`);
47
+ console.log(`Point fmt check passed: ${input}`);
47
48
  return;
48
49
  }
49
50
 
@@ -57,24 +58,34 @@ export async function main() {
57
58
  }
58
59
 
59
60
  if (command === "check-json") {
60
- 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));
61
63
  if (diagnostics.length > 0) process.exit(1);
62
64
  return;
63
65
  }
64
66
 
65
67
  if (command === "index") {
68
+ if (program.semanticSource) {
69
+ console.log(JSON.stringify(createSemanticIndex(program.semanticSource), null, 2));
70
+ return;
71
+ }
66
72
  console.log(JSON.stringify(createPointCoreIndex(program), null, 2));
67
73
  return;
68
74
  }
69
75
 
70
76
  if (command === "explain") {
71
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
+ }
72
82
  console.log(JSON.stringify(explainPointCoreRef(program, ref), null, 2));
73
83
  return;
74
84
  }
75
85
 
76
86
  if (command === "repair-plan") {
77
- console.log(JSON.stringify(createPointCoreRepairPlan(diagnostics), null, 2));
87
+ const outputDiagnostics = mapPublicDiagnostics(program, diagnostics);
88
+ console.log(JSON.stringify(createPointCoreRepairPlan(outputDiagnostics), null, 2));
78
89
  if (diagnostics.length > 0) process.exit(1);
79
90
  return;
80
91
  }
@@ -108,30 +119,70 @@ export async function main() {
108
119
  return;
109
120
  }
110
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
+
111
168
  throw new Error(`Unknown point core command: ${command}`);
112
169
  }
113
170
 
114
171
  async function runProjectCommand(command: string) {
115
172
  const inputs = await discoverInputs();
116
- 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(", ")}`);
117
174
  const results = await Promise.all(inputs.map((input) => loadCoreFile(input)));
175
+ const graph = createModuleGraph(results);
176
+ const orderedResults = orderByDependencies(results, graph);
118
177
 
119
178
  if (command === "fmt-all") {
120
- await Promise.all(
121
- results.map((result) =>
122
- isSemanticPointSyntax(result.source)
123
- ? Promise.resolve()
124
- : Bun.write(resolve(process.cwd(), result.input), formatPointCore(result.program)),
125
- ),
126
- );
127
- 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`);
128
181
  return;
129
182
  }
130
183
 
131
184
  if (command === "fmt-check-all") {
132
- const unformatted = results.filter(
133
- (result) => !isSemanticPointSyntax(result.source) && result.source !== formatPointCore(result.program),
134
- );
185
+ const unformatted = results.filter((result) => result.source !== formatPointSource(result.source));
135
186
  if (unformatted.length > 0) {
136
187
  console.error(JSON.stringify({ ok: false, unformatted: unformatted.map((result) => result.input) }, null, 2));
137
188
  process.exit(1);
@@ -141,26 +192,40 @@ async function runProjectCommand(command: string) {
141
192
  }
142
193
 
143
194
  if (command === "check-all") {
144
- const diagnostics = results.flatMap((result) =>
145
- checkPointCore(result.program).map((diagnostic) => ({ ...diagnostic, file: result.input })),
146
- );
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);
147
212
  if (diagnostics.length > 0) {
148
213
  console.error(JSON.stringify({ ok: false, diagnostics }, null, 2));
149
214
  process.exit(1);
150
215
  }
151
- console.log(`Point core check passed: ${results.length} files`);
216
+ console.log(`Point core check passed: ${results.length} files${skipped ? ` (${skipped} cached)` : ""}`);
152
217
  return;
153
218
  }
154
219
 
155
220
  if (command === "build-all") {
156
- const diagnostics = results.flatMap((result) =>
157
- 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 })),
158
223
  );
159
224
  if (diagnostics.length > 0) {
160
225
  console.error(JSON.stringify({ ok: false, diagnostics }, null, 2));
161
226
  process.exit(1);
162
227
  }
163
- for (const result of results) {
228
+ for (const result of orderedResults) {
164
229
  const output = outputFor(result.input);
165
230
  const outputPath = resolve(process.cwd(), output);
166
231
  await Bun.$`mkdir -p ${dirname(outputPath)}`.quiet();
@@ -171,46 +236,262 @@ async function runProjectCommand(command: string) {
171
236
  }
172
237
 
173
238
  if (command === "build-ts-all") {
174
- const diagnostics = results.flatMap((result) =>
175
- 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 })),
176
241
  );
177
242
  if (diagnostics.length > 0) {
178
243
  console.error(JSON.stringify({ ok: false, diagnostics }, null, 2));
179
244
  process.exit(1);
180
245
  }
181
- for (const result of results) {
246
+ for (const result of orderedResults) {
182
247
  const output = tsOutputFor(result.input);
183
248
  const outputPath = resolve(process.cwd(), output);
184
249
  await Bun.$`mkdir -p ${dirname(outputPath)}`.quiet();
185
- await Bun.write(outputPath, emitPointCoreTypeScript(result.program));
250
+ await Bun.write(outputPath, emitPointCoreTypeScript(programWithTypeScriptImports(result, graph)));
186
251
  }
187
252
  console.log(`Point core TypeScript build wrote ${results.length} files`);
188
253
  return;
189
254
  }
190
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
+
191
292
  throw new Error(`Unknown point core command: ${command}`);
192
293
  }
193
294
 
194
295
  async function discoverInputs(): Promise<string[]> {
195
- const glob = new Bun.Glob(DEFAULT_PATTERN);
196
- const inputs: string[] = [];
197
- for await (const input of glob.scan({ cwd: process.cwd(), onlyFiles: true })) {
198
- 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
+ }
199
302
  }
200
- 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;
201
383
  }
202
384
 
203
385
  async function loadCoreFile(input: string) {
204
386
  const source = await Bun.file(resolve(process.cwd(), input)).text();
205
- 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("\\", "/");
206
478
  }
207
479
 
208
480
  function outputFor(input: string): string {
209
- const name = input.split("/").pop()?.replace(/\.point$/, "") ?? "program";
481
+ const name = outputBaseName(input);
210
482
  return `${GENERATED_DIR}/${name}.ast.json`;
211
483
  }
212
484
 
213
485
  function tsOutputFor(input: string): string {
214
- const name = input.split("/").pop()?.replace(/\.point$/, "") ?? "program";
486
+ const name = outputBaseName(input);
215
487
  return `${GENERATED_DIR}/${name}.ts`;
216
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
+ }