@hatchingpoint/point 0.0.5 → 0.0.7

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,216 +1,497 @@
1
- import { dirname, resolve } from "node:path";
2
- import { checkPointCore } from "./check.ts";
3
- import { createPointCoreIndex, createPointCoreRepairPlan, explainPointCoreRef } from "./context.ts";
4
- import { emitPointCoreTypeScript } from "./emit-typescript.ts";
5
- import { formatPointCore } from "./format.ts";
6
- import { isSemanticPointSyntax, parsePointCore } from "./parser.ts";
7
-
8
- const DEFAULT_INPUT = "examples/math.point";
9
- const DEFAULT_OUTPUT = "generated/math.ast.json";
10
- const DEFAULT_TS_OUTPUT = "generated/math.ts";
11
- const DEFAULT_PATTERN = "examples/**/*.point";
12
- const GENERATED_DIR = "generated";
13
-
14
- export async function main() {
15
- const [, , command = "check", input = DEFAULT_INPUT, output = DEFAULT_OUTPUT] = Bun.argv;
16
- if (command.endsWith("-all")) {
17
- await runProjectCommand(command);
18
- return;
19
- }
20
-
21
- const inputPath = resolve(process.cwd(), input);
22
- const source = await Bun.file(inputPath).text();
23
- const program = parsePointCore(source);
24
- const diagnostics = checkPointCore(program);
25
-
26
- 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}`);
33
- return;
34
- }
35
-
36
- 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}`);
44
- process.exit(1);
45
- }
46
- console.log(`Point core fmt check passed: ${input}`);
47
- return;
48
- }
49
-
50
- if (command === "check") {
51
- if (diagnostics.length > 0) {
52
- console.error(JSON.stringify({ ok: false, diagnostics }, null, 2));
53
- process.exit(1);
54
- }
55
- console.log(`Point core check passed: ${input}`);
56
- return;
57
- }
58
-
59
- if (command === "check-json") {
60
- console.log(JSON.stringify({ schemaVersion: "point.core.check.v1", ok: diagnostics.length === 0, diagnostics }, null, 2));
61
- if (diagnostics.length > 0) process.exit(1);
62
- return;
63
- }
64
-
65
- if (command === "index") {
66
- console.log(JSON.stringify(createPointCoreIndex(program), null, 2));
67
- return;
68
- }
69
-
70
- if (command === "explain") {
71
- const ref = output;
72
- console.log(JSON.stringify(explainPointCoreRef(program, ref), null, 2));
73
- return;
74
- }
75
-
76
- if (command === "repair-plan") {
77
- console.log(JSON.stringify(createPointCoreRepairPlan(diagnostics), null, 2));
78
- if (diagnostics.length > 0) process.exit(1);
79
- return;
80
- }
81
-
82
- if (command === "print-ast") {
83
- console.log(JSON.stringify(program, null, 2));
84
- return;
85
- }
86
-
87
- if (command === "build") {
88
- if (diagnostics.length > 0) {
89
- console.error(JSON.stringify({ ok: false, diagnostics }, null, 2));
90
- process.exit(1);
91
- }
92
- const outputPath = resolve(process.cwd(), output);
93
- await Bun.$`mkdir -p ${dirname(outputPath)}`.quiet();
94
- await Bun.write(outputPath, `${JSON.stringify(program, null, 2)}\n`);
95
- console.log(`Point core build wrote ${output}`);
96
- return;
97
- }
98
-
99
- if (command === "build-ts") {
100
- if (diagnostics.length > 0) {
101
- console.error(JSON.stringify({ ok: false, diagnostics }, null, 2));
102
- process.exit(1);
103
- }
104
- const outputPath = resolve(process.cwd(), output === DEFAULT_OUTPUT ? DEFAULT_TS_OUTPUT : output);
105
- await Bun.$`mkdir -p ${dirname(outputPath)}`.quiet();
106
- await Bun.write(outputPath, emitPointCoreTypeScript(program));
107
- console.log(`Point core TypeScript build wrote ${outputPath.replaceAll("\\", "/")}`);
108
- return;
109
- }
110
-
111
- throw new Error(`Unknown point core command: ${command}`);
112
- }
113
-
114
- async function runProjectCommand(command: string) {
115
- const inputs = await discoverInputs();
116
- if (inputs.length === 0) throw new Error(`No Point core files matched ${DEFAULT_PATTERN}`);
117
- const results = await Promise.all(inputs.map((input) => loadCoreFile(input)));
118
-
119
- 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`);
128
- return;
129
- }
130
-
131
- if (command === "fmt-check-all") {
132
- const unformatted = results.filter(
133
- (result) => !isSemanticPointSyntax(result.source) && result.source !== formatPointCore(result.program),
134
- );
135
- if (unformatted.length > 0) {
136
- console.error(JSON.stringify({ ok: false, unformatted: unformatted.map((result) => result.input) }, null, 2));
137
- process.exit(1);
138
- }
139
- console.log(`Point core fmt check passed: ${results.length} files`);
140
- return;
141
- }
142
-
143
- if (command === "check-all") {
144
- const diagnostics = results.flatMap((result) =>
145
- checkPointCore(result.program).map((diagnostic) => ({ ...diagnostic, file: result.input })),
146
- );
147
- if (diagnostics.length > 0) {
148
- console.error(JSON.stringify({ ok: false, diagnostics }, null, 2));
149
- process.exit(1);
150
- }
151
- console.log(`Point core check passed: ${results.length} files`);
152
- return;
153
- }
154
-
155
- if (command === "build-all") {
156
- const diagnostics = results.flatMap((result) =>
157
- checkPointCore(result.program).map((diagnostic) => ({ ...diagnostic, file: result.input })),
158
- );
159
- if (diagnostics.length > 0) {
160
- console.error(JSON.stringify({ ok: false, diagnostics }, null, 2));
161
- process.exit(1);
162
- }
163
- for (const result of results) {
164
- const output = outputFor(result.input);
165
- const outputPath = resolve(process.cwd(), output);
166
- await Bun.$`mkdir -p ${dirname(outputPath)}`.quiet();
167
- await Bun.write(outputPath, `${JSON.stringify(result.program, null, 2)}\n`);
168
- }
169
- console.log(`Point core build wrote ${results.length} files`);
170
- return;
171
- }
172
-
173
- if (command === "build-ts-all") {
174
- const diagnostics = results.flatMap((result) =>
175
- checkPointCore(result.program).map((diagnostic) => ({ ...diagnostic, file: result.input })),
176
- );
177
- if (diagnostics.length > 0) {
178
- console.error(JSON.stringify({ ok: false, diagnostics }, null, 2));
179
- process.exit(1);
180
- }
181
- for (const result of results) {
182
- const output = tsOutputFor(result.input);
183
- const outputPath = resolve(process.cwd(), output);
184
- await Bun.$`mkdir -p ${dirname(outputPath)}`.quiet();
185
- await Bun.write(outputPath, emitPointCoreTypeScript(result.program));
186
- }
187
- console.log(`Point core TypeScript build wrote ${results.length} files`);
188
- return;
189
- }
190
-
191
- throw new Error(`Unknown point core command: ${command}`);
192
- }
193
-
194
- 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("\\", "/"));
199
- }
200
- return inputs.sort((a, b) => a.localeCompare(b));
201
- }
202
-
203
- async function loadCoreFile(input: string) {
204
- const source = await Bun.file(resolve(process.cwd(), input)).text();
205
- return { input, source, program: parsePointCore(source) };
206
- }
207
-
208
- function outputFor(input: string): string {
209
- const name = input.split("/").pop()?.replace(/\.point$/, "") ?? "program";
210
- return `${GENERATED_DIR}/${name}.ast.json`;
211
- }
212
-
213
- function tsOutputFor(input: string): string {
214
- const name = input.split("/").pop()?.replace(/\.point$/, "") ?? "program";
215
- return `${GENERATED_DIR}/${name}.ts`;
216
- }
1
+ import { dirname, resolve } from "node:path";
2
+ import { tmpdir } from "node:os";
3
+ import type { PointCoreDeclaration, PointCoreProgram } from "./ast.ts";
4
+ import { checkPointCore } from "./check.ts";
5
+ import { createPointCoreIndex, createPointCoreRepairPlan, explainPointCoreRef } from "./context.ts";
6
+ import { createSemanticIndex, explainSemanticRef, mapPublicDiagnostics } from "../semantic/context.ts";
7
+ import { emitPointCoreTypeScript } from "./emit-typescript.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";
12
+
13
+ const DEFAULT_INPUT = "examples/math.point";
14
+ const DEFAULT_OUTPUT = "generated/math.ast.json";
15
+ const DEFAULT_TS_OUTPUT = "generated/math.ts";
16
+ const DEFAULT_PATTERNS = ["examples/**/*.point", "std/**/*.point", "compiler/**/*.point"];
17
+ const GENERATED_DIR = "generated";
18
+
19
+ export async function main() {
20
+ const [, , command = "check", input = DEFAULT_INPUT, output = DEFAULT_OUTPUT] = Bun.argv;
21
+ if (command.endsWith("-all")) {
22
+ await runProjectCommand(command);
23
+ return;
24
+ }
25
+
26
+ if (command === "repl") {
27
+ await runRepl(Bun.argv.slice(3).join(" "));
28
+ return;
29
+ }
30
+
31
+ const inputPath = resolve(process.cwd(), input);
32
+ const source = await Bun.file(inputPath).text();
33
+ const program = parsePointSource(source);
34
+ const diagnostics = checkPointCore(program);
35
+
36
+ if (command === "fmt") {
37
+ await Bun.write(inputPath, formatPointSource(source));
38
+ console.log(`Point fmt wrote ${input}`);
39
+ return;
40
+ }
41
+
42
+ if (command === "fmt-check") {
43
+ if (source !== formatPointSource(source)) {
44
+ console.error(`Point fmt check failed: ${input}`);
45
+ process.exit(1);
46
+ }
47
+ console.log(`Point fmt check passed: ${input}`);
48
+ return;
49
+ }
50
+
51
+ if (command === "check") {
52
+ if (diagnostics.length > 0) {
53
+ console.error(JSON.stringify({ ok: false, diagnostics }, null, 2));
54
+ process.exit(1);
55
+ }
56
+ console.log(`Point core check passed: ${input}`);
57
+ return;
58
+ }
59
+
60
+ if (command === "check-json") {
61
+ const outputDiagnostics = mapPublicDiagnostics(program, diagnostics);
62
+ console.log(JSON.stringify({ schemaVersion: "point.core.check.v1", ok: diagnostics.length === 0, diagnostics: outputDiagnostics }, null, 2));
63
+ if (diagnostics.length > 0) process.exit(1);
64
+ return;
65
+ }
66
+
67
+ if (command === "index") {
68
+ if (program.semanticSource) {
69
+ console.log(JSON.stringify(createSemanticIndex(program.semanticSource), null, 2));
70
+ return;
71
+ }
72
+ console.log(JSON.stringify(createPointCoreIndex(program), null, 2));
73
+ return;
74
+ }
75
+
76
+ if (command === "explain") {
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
+ }
82
+ console.log(JSON.stringify(explainPointCoreRef(program, ref), null, 2));
83
+ return;
84
+ }
85
+
86
+ if (command === "repair-plan") {
87
+ const outputDiagnostics = mapPublicDiagnostics(program, diagnostics);
88
+ console.log(JSON.stringify(createPointCoreRepairPlan(outputDiagnostics), null, 2));
89
+ if (diagnostics.length > 0) process.exit(1);
90
+ return;
91
+ }
92
+
93
+ if (command === "print-ast") {
94
+ console.log(JSON.stringify(program, null, 2));
95
+ return;
96
+ }
97
+
98
+ if (command === "build") {
99
+ if (diagnostics.length > 0) {
100
+ console.error(JSON.stringify({ ok: false, diagnostics }, null, 2));
101
+ process.exit(1);
102
+ }
103
+ const outputPath = resolve(process.cwd(), output);
104
+ await Bun.$`mkdir -p ${dirname(outputPath)}`.quiet();
105
+ await Bun.write(outputPath, `${JSON.stringify(program, null, 2)}\n`);
106
+ console.log(`Point core build wrote ${output}`);
107
+ return;
108
+ }
109
+
110
+ if (command === "build-ts") {
111
+ if (diagnostics.length > 0) {
112
+ console.error(JSON.stringify({ ok: false, diagnostics }, null, 2));
113
+ process.exit(1);
114
+ }
115
+ const outputPath = resolve(process.cwd(), output === DEFAULT_OUTPUT ? DEFAULT_TS_OUTPUT : output);
116
+ await Bun.$`mkdir -p ${dirname(outputPath)}`.quiet();
117
+ await Bun.write(outputPath, emitPointCoreTypeScript(program));
118
+ console.log(`Point core TypeScript build wrote ${outputPath.replaceAll("\\", "/")}`);
119
+ return;
120
+ }
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
+
168
+ throw new Error(`Unknown point core command: ${command}`);
169
+ }
170
+
171
+ async function runProjectCommand(command: string) {
172
+ const inputs = await discoverInputs();
173
+ if (inputs.length === 0) throw new Error(`No Point core files matched ${DEFAULT_PATTERNS.join(", ")}`);
174
+ const results = await Promise.all(inputs.map((input) => loadCoreFile(input)));
175
+ const graph = createModuleGraph(results);
176
+ const orderedResults = orderByDependencies(results, graph);
177
+
178
+ if (command === "fmt-all") {
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`);
181
+ return;
182
+ }
183
+
184
+ if (command === "fmt-check-all") {
185
+ const unformatted = results.filter((result) => result.source !== formatPointSource(result.source));
186
+ if (unformatted.length > 0) {
187
+ console.error(JSON.stringify({ ok: false, unformatted: unformatted.map((result) => result.input) }, null, 2));
188
+ process.exit(1);
189
+ }
190
+ console.log(`Point core fmt check passed: ${results.length} files`);
191
+ return;
192
+ }
193
+
194
+ if (command === "check-all") {
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);
212
+ if (diagnostics.length > 0) {
213
+ console.error(JSON.stringify({ ok: false, diagnostics }, null, 2));
214
+ process.exit(1);
215
+ }
216
+ console.log(`Point core check passed: ${results.length} files${skipped ? ` (${skipped} cached)` : ""}`);
217
+ return;
218
+ }
219
+
220
+ if (command === "build-all") {
221
+ const diagnostics = orderedResults.flatMap((result) =>
222
+ checkPointCore(programWithDependencyDeclarations(result, graph)).map((diagnostic) => ({ ...diagnostic, file: result.input })),
223
+ );
224
+ if (diagnostics.length > 0) {
225
+ console.error(JSON.stringify({ ok: false, diagnostics }, null, 2));
226
+ process.exit(1);
227
+ }
228
+ for (const result of orderedResults) {
229
+ const output = outputFor(result.input);
230
+ const outputPath = resolve(process.cwd(), output);
231
+ await Bun.$`mkdir -p ${dirname(outputPath)}`.quiet();
232
+ await Bun.write(outputPath, `${JSON.stringify(result.program, null, 2)}\n`);
233
+ }
234
+ console.log(`Point core build wrote ${results.length} files`);
235
+ return;
236
+ }
237
+
238
+ if (command === "build-ts-all") {
239
+ const diagnostics = orderedResults.flatMap((result) =>
240
+ checkPointCore(programWithDependencyDeclarations(result, graph)).map((diagnostic) => ({ ...diagnostic, file: result.input })),
241
+ );
242
+ if (diagnostics.length > 0) {
243
+ console.error(JSON.stringify({ ok: false, diagnostics }, null, 2));
244
+ process.exit(1);
245
+ }
246
+ for (const result of orderedResults) {
247
+ const output = tsOutputFor(result.input);
248
+ const outputPath = resolve(process.cwd(), output);
249
+ await Bun.$`mkdir -p ${dirname(outputPath)}`.quiet();
250
+ await Bun.write(outputPath, emitPointCoreTypeScript(programWithTypeScriptImports(result, graph)));
251
+ }
252
+ console.log(`Point core TypeScript build wrote ${results.length} files`);
253
+ return;
254
+ }
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
+
292
+ throw new Error(`Unknown point core command: ${command}`);
293
+ }
294
+
295
+ async function discoverInputs(): Promise<string[]> {
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
+ }
302
+ }
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;
383
+ }
384
+
385
+ async function loadCoreFile(input: string) {
386
+ const source = await Bun.file(resolve(process.cwd(), input)).text();
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("\\", "/");
478
+ }
479
+
480
+ function outputFor(input: string): string {
481
+ const name = outputBaseName(input);
482
+ return `${GENERATED_DIR}/${name}.ast.json`;
483
+ }
484
+
485
+ function tsOutputFor(input: string): string {
486
+ const name = outputBaseName(input);
487
+ return `${GENERATED_DIR}/${name}.ts`;
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
+ }