@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/README.md +18 -5
- package/package.json +1 -1
- package/src/core/ast.ts +40 -2
- package/src/core/check.ts +178 -67
- package/src/core/cli.ts +332 -51
- package/src/core/context.ts +213 -36
- package/src/core/emit-javascript.ts +124 -0
- package/src/core/emit-typescript.ts +38 -5
- package/src/core/format.ts +4 -102
- package/src/core/incremental.ts +53 -0
- package/src/core/index.ts +5 -0
- package/src/core/lexer.ts +11 -6
- package/src/core/parser.ts +11 -612
- package/src/core/semantic-source.ts +26 -0
- package/src/core/serialize.ts +18 -0
- package/src/core/test-only/core-text-parser.ts +415 -0
- package/src/core/test-only/format-core.ts +120 -0
- package/src/core/test-only/index.ts +3 -0
- package/src/core/test-only/legacy-lowering.ts +1047 -0
- package/src/semantic/ast.ts +230 -0
- package/src/semantic/callables.ts +51 -0
- package/src/semantic/context.ts +347 -0
- package/src/semantic/desugar.ts +665 -0
- package/src/semantic/expressions.ts +347 -0
- package/src/semantic/format.ts +222 -0
- package/src/semantic/index.ts +10 -0
- package/src/semantic/metadata.ts +37 -0
- package/src/semantic/naming.ts +33 -0
- package/src/semantic/parse.ts +945 -0
- package/src/semantic/serialize.ts +18 -0
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 {
|
|
6
|
-
import {
|
|
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
|
|
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 =
|
|
33
|
+
const program = parsePointSource(source);
|
|
24
34
|
const diagnostics = checkPointCore(program);
|
|
25
35
|
|
|
26
36
|
if (command === "fmt") {
|
|
27
|
-
|
|
28
|
-
|
|
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 (
|
|
38
|
-
console.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 ${
|
|
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
|
-
|
|
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
|
|
145
|
-
|
|
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 =
|
|
157
|
-
checkPointCore(result
|
|
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
|
|
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 =
|
|
175
|
-
checkPointCore(result
|
|
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
|
|
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
|
|
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
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
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:
|
|
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
|
|
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
|
|
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
|
+
}
|