@hatchingpoint/point 0.0.12 → 0.0.14
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/package.json +8 -2
- package/src/cli.ts +0 -0
- package/src/core/ast.ts +21 -1
- package/src/core/check.ts +10 -2
- package/src/core/cli.ts +87 -23
- package/src/core/context.ts +3 -0
- package/src/core/emit-javascript.ts +5 -1
- package/src/core/emit-python.ts +22 -7
- package/src/core/emit-typescript.ts +84 -3
- package/src/core/index.ts +1 -0
- package/src/core/packages.ts +230 -0
- package/src/core/run-bridge.ts +93 -0
- package/src/core/semantic-source.ts +2 -2
- package/src/core/test-only/legacy-lowering.ts +76 -7
- package/src/semantic/ast.ts +14 -1
- package/src/semantic/callables.ts +2 -1
- package/src/semantic/context.ts +12 -1
- package/src/semantic/desugar.ts +82 -3
- package/src/semantic/expressions.ts +8 -0
- package/src/semantic/format.ts +10 -0
- package/src/semantic/metadata.ts +3 -0
- package/src/semantic/naming.ts +4 -2
- package/src/semantic/parse.ts +81 -2
- package/src/std/env.ts +4 -0
- package/src/std/fs.ts +19 -0
- package/src/std/http.ts +30 -0
- package/src/std/json.ts +13 -0
- package/src/std/text.ts +15 -0
- package/src/std/time.ts +15 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hatchingpoint/point",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.14",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Point language compiler and CLI.",
|
|
@@ -24,7 +24,13 @@
|
|
|
24
24
|
"exports": {
|
|
25
25
|
".": "./src/index.ts",
|
|
26
26
|
"./cli": "./src/cli.ts",
|
|
27
|
-
"./core": "./src/core/index.ts"
|
|
27
|
+
"./core": "./src/core/index.ts",
|
|
28
|
+
"./std/json": "./src/std/json.ts",
|
|
29
|
+
"./std/http": "./src/std/http.ts",
|
|
30
|
+
"./std/text": "./src/std/text.ts",
|
|
31
|
+
"./std/time": "./src/std/time.ts",
|
|
32
|
+
"./std/fs": "./src/std/fs.ts",
|
|
33
|
+
"./std/env": "./src/std/env.ts"
|
|
28
34
|
},
|
|
29
35
|
"publishConfig": {
|
|
30
36
|
"access": "public",
|
package/src/cli.ts
CHANGED
|
File without changes
|
package/src/core/ast.ts
CHANGED
|
@@ -85,11 +85,31 @@ export interface PointSemanticProgramMetadata {
|
|
|
85
85
|
source: "semantic";
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
export interface PointSemanticPageLayout {
|
|
89
|
+
title: PointCoreExpression;
|
|
90
|
+
description?: PointCoreExpression;
|
|
91
|
+
main: PointCoreExpression;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface PointSemanticViewCheckboxBinding {
|
|
95
|
+
label: string;
|
|
96
|
+
target: PointCoreExpression;
|
|
97
|
+
recordParam: string;
|
|
98
|
+
fieldName: string;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface PointSemanticViewControls {
|
|
102
|
+
changeCallback: string;
|
|
103
|
+
checkboxes: PointSemanticViewCheckboxBinding[];
|
|
104
|
+
}
|
|
105
|
+
|
|
88
106
|
export interface PointSemanticDeclarationMetadata {
|
|
89
|
-
kind: "record" | "calculation" | "rule" | "label" | "external" | "action" | "policy" | "view" | "route" | "workflow" | "command";
|
|
107
|
+
kind: "record" | "calculation" | "rule" | "label" | "external" | "action" | "policy" | "view" | "page" | "route" | "workflow" | "command";
|
|
90
108
|
name: string;
|
|
91
109
|
outputName?: string;
|
|
92
110
|
effects?: string[];
|
|
111
|
+
pageLayout?: PointSemanticPageLayout;
|
|
112
|
+
viewControls?: PointSemanticViewControls;
|
|
93
113
|
}
|
|
94
114
|
|
|
95
115
|
export interface PointCoreTypeExpression {
|
package/src/core/check.ts
CHANGED
|
@@ -28,7 +28,7 @@ type DiagnosticMetadata = Partial<Pick<PointCoreDiagnostic, "expected" | "actual
|
|
|
28
28
|
type ScopeEntry = { type: PointCoreTypeExpression; mutable: boolean };
|
|
29
29
|
type Scope = Map<string, ScopeEntry>;
|
|
30
30
|
|
|
31
|
-
const PRIMITIVE_TYPES = new Set(["Text", "Int", "Float", "Bool", "Void", "List", "Maybe", "Error", "Or"]);
|
|
31
|
+
const PRIMITIVE_TYPES = new Set(["Text", "Int", "Float", "Bool", "Void", "List", "Maybe", "Error", "Or", "Page", "Handler"]);
|
|
32
32
|
|
|
33
33
|
export function checkPointCore(program: PointCoreProgram): PointCoreDiagnostic[] {
|
|
34
34
|
const checker = new CoreChecker(program);
|
|
@@ -125,6 +125,7 @@ class CoreChecker {
|
|
|
125
125
|
}
|
|
126
126
|
return;
|
|
127
127
|
}
|
|
128
|
+
if (fn.semantic?.kind === "page" || fn.semantic?.kind === "view") return;
|
|
128
129
|
this.checkExpressionAssignable(statement.value, fn.returnType, `fn.${fn.name}.return`, locals);
|
|
129
130
|
return;
|
|
130
131
|
}
|
|
@@ -531,7 +532,14 @@ class CoreChecker {
|
|
|
531
532
|
repair: "Use syntax such as User or Error.",
|
|
532
533
|
});
|
|
533
534
|
}
|
|
534
|
-
if (type.name
|
|
535
|
+
if (type.name === "Handler" && type.args.length !== 1) {
|
|
536
|
+
this.push("invalid-type-arity", "Handler requires one type argument", path, type.span, {
|
|
537
|
+
expected: "Handler T",
|
|
538
|
+
actual: formatType(type),
|
|
539
|
+
repair: "Use Handler Listing Signals or another record type.",
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
if (type.name !== "List" && type.name !== "Maybe" && type.name !== "Or" && type.name !== "Handler" && type.args.length > 0 && !this.typeDeclarations.has(String(type.name))) {
|
|
535
543
|
this.push("invalid-type-arity", `${type.name} does not accept type arguments`, path, type.span, {
|
|
536
544
|
expected: String(type.name),
|
|
537
545
|
actual: formatType(type),
|
package/src/core/cli.ts
CHANGED
|
@@ -6,11 +6,13 @@ import { createPointCoreIndex, createPointCoreRepairPlan, explainPointCoreRef }
|
|
|
6
6
|
import { createSemanticIndex, explainSemanticRef, mapPublicDiagnostics } from "../semantic/context.ts";
|
|
7
7
|
import { emitPointCoreTypeScript } from "./emit-typescript.ts";
|
|
8
8
|
import { emitPointCoreJavaScript } from "./emit-javascript.ts";
|
|
9
|
-
import { emitPointCorePython } from "./emit-python.ts";
|
|
9
|
+
import { emitPointCorePython, isPureLogicProgram } from "./emit-python.ts";
|
|
10
|
+
import { canBundleRunInMemory, executeBundledEntry } from "./run-bridge.ts";
|
|
10
11
|
import { formatPointSource } from "./format.ts";
|
|
11
12
|
import { isCacheHit, isIncrementalEnabled, readBuildCache, recordCacheEntry, writeBuildCache } from "./incremental.ts";
|
|
12
13
|
import { parsePointSource } from "./parser.ts";
|
|
13
14
|
import { runCheckDocs } from "./check-docs.ts";
|
|
15
|
+
import { addPointDependency, modulePathFromLock, POINT_LOCK, POINT_MANIFEST, readPointLock } from "./packages.ts";
|
|
14
16
|
import { runPointLspServer } from "../lsp/server.ts";
|
|
15
17
|
|
|
16
18
|
const DEFAULT_INPUT = "examples/math.point";
|
|
@@ -22,7 +24,17 @@ const DEFAULT_PATTERNS = ["examples/**/*.point", "std/**/*.point", "compiler/**/
|
|
|
22
24
|
const GENERATED_DIR = "generated";
|
|
23
25
|
|
|
24
26
|
export async function main() {
|
|
25
|
-
const
|
|
27
|
+
const command = Bun.argv[2] ?? "check";
|
|
28
|
+
const tail = Bun.argv.slice(3);
|
|
29
|
+
let input = tail[0] ?? DEFAULT_INPUT;
|
|
30
|
+
let output = tail[1] ?? DEFAULT_OUTPUT;
|
|
31
|
+
let runFlags: Record<string, boolean | undefined> | undefined;
|
|
32
|
+
if (command === "run") {
|
|
33
|
+
const parsed = parseCliFlags(tail);
|
|
34
|
+
runFlags = parsed.flags;
|
|
35
|
+
input = parsed.positional[0] ?? DEFAULT_INPUT;
|
|
36
|
+
output = parsed.positional[1] ?? DEFAULT_OUTPUT;
|
|
37
|
+
}
|
|
26
38
|
if (command.endsWith("-all")) {
|
|
27
39
|
await runProjectCommand(command);
|
|
28
40
|
return;
|
|
@@ -43,6 +55,18 @@ export async function main() {
|
|
|
43
55
|
return;
|
|
44
56
|
}
|
|
45
57
|
|
|
58
|
+
if (command === "add") {
|
|
59
|
+
const dependencyName = input;
|
|
60
|
+
const spec = output;
|
|
61
|
+
if (!dependencyName || !spec) {
|
|
62
|
+
throw new Error("Usage: point add <name> <spec> (spec: workspace:<path> | file:<path> | npm:<package>)");
|
|
63
|
+
}
|
|
64
|
+
const { manifest, lock } = await addPointDependency(dependencyName, spec);
|
|
65
|
+
console.log(`Point add updated ${POINT_MANIFEST} and ${POINT_LOCK}: ${dependencyName} -> ${spec}`);
|
|
66
|
+
console.log(JSON.stringify({ name: manifest.name, dependencies: manifest.dependencies, lockPackages: Object.keys(lock.packages) }, null, 2));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
46
70
|
const inputPath = resolve(process.cwd(), input);
|
|
47
71
|
const source = await Bun.file(inputPath).text();
|
|
48
72
|
const program = parsePointSource(source);
|
|
@@ -163,16 +187,17 @@ export async function main() {
|
|
|
163
187
|
console.error(JSON.stringify({ ok: false, diagnostics }, null, 2));
|
|
164
188
|
process.exit(1);
|
|
165
189
|
}
|
|
166
|
-
const runOutput = resolve(tmpdir(), `point-run-${Date.now()}.js`);
|
|
167
|
-
await Bun.write(runOutput, emitPointCoreJavaScript(program));
|
|
168
190
|
let entryName: string | null = null;
|
|
169
191
|
try {
|
|
170
|
-
const mod = await import(pathToFileUrl(runOutput));
|
|
171
192
|
entryName = findRunEntryName(program);
|
|
172
193
|
if (!entryName) throw new Error("No zero-argument entrypoint found. Define an action or calculation with no inputs.");
|
|
173
|
-
const
|
|
174
|
-
if (
|
|
175
|
-
|
|
194
|
+
const useBundle = runFlags?.bundle === true || (runFlags?.bundle !== false && canBundleRunInMemory(program));
|
|
195
|
+
if (runFlags?.bundle === true && !canBundleRunInMemory(program)) {
|
|
196
|
+
throw new Error("Cannot use --bundle: module has imports, externals, or non-pure logic (views, routes, workflows, commands).");
|
|
197
|
+
}
|
|
198
|
+
const value = useBundle
|
|
199
|
+
? await executeBundledEntry(program, entryName)
|
|
200
|
+
: await executeTempModuleRun(program, entryName);
|
|
176
201
|
if (value !== undefined) console.log(typeof value === "string" ? value : JSON.stringify(value));
|
|
177
202
|
} catch (error) {
|
|
178
203
|
console.error(`Runtime error in ${runtimeSourceLocation(program, input, entryName)}: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -198,8 +223,9 @@ export async function main() {
|
|
|
198
223
|
async function runProjectCommand(command: string) {
|
|
199
224
|
const inputs = await discoverInputs();
|
|
200
225
|
if (inputs.length === 0) throw new Error(`No Point core files matched ${DEFAULT_PATTERNS.join(", ")}`);
|
|
201
|
-
const
|
|
202
|
-
const
|
|
226
|
+
const lock = await readPointLock();
|
|
227
|
+
const results = await Promise.all(inputs.map((input) => loadCoreFile(input, lock)));
|
|
228
|
+
const graph = createModuleGraph(results, lock);
|
|
203
229
|
const orderedResults = orderByDependencies(results, graph);
|
|
204
230
|
|
|
205
231
|
if (command === "fmt-all") {
|
|
@@ -298,6 +324,26 @@ async function runProjectCommand(command: string) {
|
|
|
298
324
|
return;
|
|
299
325
|
}
|
|
300
326
|
|
|
327
|
+
if (command === "build-py-all") {
|
|
328
|
+
const diagnostics = orderedResults.flatMap((result) =>
|
|
329
|
+
checkPointCore(programWithDependencyDeclarations(result, graph)).map((diagnostic) => ({ ...diagnostic, file: result.input })),
|
|
330
|
+
);
|
|
331
|
+
if (diagnostics.length > 0) {
|
|
332
|
+
console.error(JSON.stringify({ ok: false, diagnostics }, null, 2));
|
|
333
|
+
process.exit(1);
|
|
334
|
+
}
|
|
335
|
+
const pureLogicResults = orderedResults.filter((result) => isPureLogicProgram(result.program));
|
|
336
|
+
for (const result of pureLogicResults) {
|
|
337
|
+
const output = pyOutputFor(result.input);
|
|
338
|
+
const outputPath = resolve(process.cwd(), output);
|
|
339
|
+
await Bun.$`mkdir -p ${dirname(outputPath)}`.quiet();
|
|
340
|
+
await Bun.write(outputPath, emitPointCorePython(programWithTypeScriptImports(result, graph)));
|
|
341
|
+
}
|
|
342
|
+
const skipped = results.length - pureLogicResults.length;
|
|
343
|
+
console.log(`Point core Python build wrote ${pureLogicResults.length} files${skipped ? ` (${skipped} skipped)` : ""}`);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
|
|
301
347
|
if (command === "test-all") {
|
|
302
348
|
const diagnostics = orderedResults.flatMap((result) =>
|
|
303
349
|
checkPointCore(programWithDependencyDeclarations(result, graph)).map((diagnostic) => ({ ...diagnostic, file: result.input })),
|
|
@@ -400,6 +446,26 @@ function pathToFileUrl(path: string): string {
|
|
|
400
446
|
return `file://${path.replaceAll("\\", "/")}`;
|
|
401
447
|
}
|
|
402
448
|
|
|
449
|
+
async function executeTempModuleRun(program: PointCoreProgram, entryName: string): Promise<unknown> {
|
|
450
|
+
const runOutput = resolve(tmpdir(), `point-run-${Date.now()}.js`);
|
|
451
|
+
await Bun.write(runOutput, emitPointCoreJavaScript(program));
|
|
452
|
+
const mod = await import(pathToFileUrl(runOutput));
|
|
453
|
+
const entry = mod[entryName];
|
|
454
|
+
if (typeof entry !== "function") throw new Error(`Entrypoint ${entryName} was not exported.`);
|
|
455
|
+
return await entry();
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function parseCliFlags(args: string[]): { flags: Record<string, boolean | undefined>; positional: string[] } {
|
|
459
|
+
const flags: Record<string, boolean | undefined> = {};
|
|
460
|
+
const positional: string[] = [];
|
|
461
|
+
for (const arg of args) {
|
|
462
|
+
if (arg === "--bundle") flags.bundle = true;
|
|
463
|
+
else if (arg === "--no-bundle") flags.bundle = false;
|
|
464
|
+
else positional.push(arg);
|
|
465
|
+
}
|
|
466
|
+
return { flags, positional };
|
|
467
|
+
}
|
|
468
|
+
|
|
403
469
|
export function findRunEntryName(program: PointCoreProgram): string | null {
|
|
404
470
|
const zeroArgFunctions = program.declarations.filter((declaration) => declaration.kind === "function" && declaration.params.length === 0);
|
|
405
471
|
const preferred =
|
|
@@ -409,9 +475,9 @@ export function findRunEntryName(program: PointCoreProgram): string | null {
|
|
|
409
475
|
return preferred?.name ?? null;
|
|
410
476
|
}
|
|
411
477
|
|
|
412
|
-
async function loadCoreFile(input: string) {
|
|
478
|
+
async function loadCoreFile(input: string, lock: Awaited<ReturnType<typeof readPointLock>>) {
|
|
413
479
|
const source = await Bun.file(resolve(process.cwd(), input)).text();
|
|
414
|
-
return { input, source, program: parsePointSource(source), uses: parseUseDeclarations(source, input) };
|
|
480
|
+
return { input, source, program: parsePointSource(source), uses: parseUseDeclarations(source, input, lock) };
|
|
415
481
|
}
|
|
416
482
|
|
|
417
483
|
type CoreFile = Awaited<ReturnType<typeof loadCoreFile>>;
|
|
@@ -423,15 +489,15 @@ interface UseDeclaration {
|
|
|
423
489
|
input: string;
|
|
424
490
|
}
|
|
425
491
|
|
|
426
|
-
function parseUseDeclarations(source: string, input: string): UseDeclaration[] {
|
|
492
|
+
function parseUseDeclarations(source: string, input: string, lock: Awaited<ReturnType<typeof readPointLock>>): UseDeclaration[] {
|
|
427
493
|
return source
|
|
428
494
|
.split(/\r?\n/)
|
|
429
495
|
.map((line) => line.trim().match(/^use\s+([A-Za-z][A-Za-z0-9]*(?:\.[A-Za-z][A-Za-z0-9]*)*)(?:\s+from\s+"([^"]+)")?$/))
|
|
430
496
|
.filter((match): match is RegExpMatchArray => Boolean(match))
|
|
431
|
-
.map((match) => ({ moduleName: match[1]!, from: match[2] ??
|
|
497
|
+
.map((match) => ({ moduleName: match[1]!, from: match[2] ?? modulePathFromLock(lock, match[1]!), input }));
|
|
432
498
|
}
|
|
433
499
|
|
|
434
|
-
function createModuleGraph(results: CoreFile[]): ModuleGraph {
|
|
500
|
+
function createModuleGraph(results: CoreFile[], lock: Awaited<ReturnType<typeof readPointLock>>): ModuleGraph {
|
|
435
501
|
const byInput = new Map(results.map((result) => [normalizeInput(result.input), result]));
|
|
436
502
|
const graph: ModuleGraph = new Map();
|
|
437
503
|
for (const result of results) {
|
|
@@ -490,14 +556,12 @@ function publicDeclarations(program: PointCoreProgram): Array<Extract<PointCoreD
|
|
|
490
556
|
}
|
|
491
557
|
|
|
492
558
|
function resolveDependencyInput(input: string, from: string): string {
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
if (!moduleName.startsWith("std.")) throw new Error(`Use declarations without from must target std modules: ${moduleName}`);
|
|
500
|
-
return `${moduleName.replace(/^std\./, "std/").replaceAll(".", "/")}.point`;
|
|
559
|
+
const normalized = from.replaceAll("\\", "/");
|
|
560
|
+
if (normalized.startsWith("./") || normalized.startsWith("../")) {
|
|
561
|
+
const base = dirname(resolve(process.cwd(), input));
|
|
562
|
+
return resolve(base, from).replace(resolve(process.cwd()), "").replace(/^[/\\]/, "");
|
|
563
|
+
}
|
|
564
|
+
return normalized;
|
|
501
565
|
}
|
|
502
566
|
|
|
503
567
|
function normalizeInput(input: string): string {
|
package/src/core/context.ts
CHANGED
|
@@ -27,6 +27,7 @@ export type PointCoreSymbolKind =
|
|
|
27
27
|
| "action"
|
|
28
28
|
| "policy"
|
|
29
29
|
| "view"
|
|
30
|
+
| "page"
|
|
30
31
|
| "route"
|
|
31
32
|
| "workflow"
|
|
32
33
|
| "command";
|
|
@@ -325,6 +326,7 @@ function relatedRefsFor(symbol: PointCoreSymbol, index: PointCoreIndex): string[
|
|
|
325
326
|
symbol.kind === "action" ||
|
|
326
327
|
symbol.kind === "policy" ||
|
|
327
328
|
symbol.kind === "view" ||
|
|
329
|
+
symbol.kind === "page" ||
|
|
328
330
|
symbol.kind === "route" ||
|
|
329
331
|
symbol.kind === "workflow" ||
|
|
330
332
|
symbol.kind === "command"
|
|
@@ -351,6 +353,7 @@ function summaryFor(symbol: PointCoreSymbol): string {
|
|
|
351
353
|
if (symbol.kind === "action") return `Semantic action ${symbol.name} returns ${symbol.type}; effects: ${(symbol.effects ?? []).join(", ") || "none"}.`;
|
|
352
354
|
if (symbol.kind === "policy") return `Semantic policy ${symbol.name} returns ${symbol.type}.`;
|
|
353
355
|
if (symbol.kind === "view") return `Semantic view ${symbol.name} returns React JSX.`;
|
|
356
|
+
if (symbol.kind === "page") return `Semantic page ${symbol.name} returns a Next.js page shell as React JSX.`;
|
|
354
357
|
if (symbol.kind === "route") return `Semantic route ${symbol.name} returns ${symbol.type}.`;
|
|
355
358
|
if (symbol.kind === "workflow") return `Semantic workflow ${symbol.name} returns ${symbol.type}.`;
|
|
356
359
|
if (symbol.kind === "command") return `Semantic command ${symbol.name} returns ${symbol.type}.`;
|
|
@@ -52,9 +52,13 @@ function emitDeclaration(declaration: PointCoreDeclaration): string[] {
|
|
|
52
52
|
|
|
53
53
|
function emitFunction(declaration: PointCoreFunctionDeclaration): string[] {
|
|
54
54
|
const asyncPrefix = declaration.semantic?.kind === "action" || declaration.semantic?.kind === "workflow" || declaration.semantic?.kind === "command" ? "async " : "";
|
|
55
|
+
const bodyLines =
|
|
56
|
+
declaration.semantic?.kind === "page" && declaration.semantic.pageLayout
|
|
57
|
+
? [`return ${emitExpression(declaration.semantic.pageLayout.main)};`]
|
|
58
|
+
: declaration.body.flatMap((statement) => emitStatement(statement, declaration.semantic?.kind));
|
|
55
59
|
return [
|
|
56
60
|
`export ${asyncPrefix}function ${declaration.name}(${declaration.params.map(emitParam).join(", ")}) {`,
|
|
57
|
-
...indentLines(
|
|
61
|
+
...indentLines(bodyLines),
|
|
58
62
|
"}",
|
|
59
63
|
];
|
|
60
64
|
}
|
package/src/core/emit-python.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
PointCoreDeclaration,
|
|
3
3
|
PointCoreExpression,
|
|
4
|
+
PointCoreExternalDeclaration,
|
|
4
5
|
PointCoreFunctionDeclaration,
|
|
5
6
|
PointCoreParameter,
|
|
6
7
|
PointCorePrimitiveType,
|
|
@@ -16,7 +17,12 @@ const BINARY_OPERATORS: Record<string, string> = {
|
|
|
16
17
|
or: "or",
|
|
17
18
|
};
|
|
18
19
|
|
|
19
|
-
const UNSUPPORTED_SEMANTIC_KINDS = new Set(["view", "
|
|
20
|
+
const UNSUPPORTED_SEMANTIC_KINDS = new Set(["view", "page", "route", "workflow", "command"]);
|
|
21
|
+
|
|
22
|
+
/** True when every declaration is emit-able as Python (no views, routes, workflows, or commands). */
|
|
23
|
+
export function isPureLogicProgram(program: PointCoreProgram): boolean {
|
|
24
|
+
return !program.declarations.some((declaration) => declaration.semantic && UNSUPPORTED_SEMANTIC_KINDS.has(declaration.semantic.kind));
|
|
25
|
+
}
|
|
20
26
|
|
|
21
27
|
/** Emit Python from a core AST program (pure logic modules). Production path: parsePointSource → check → emit. */
|
|
22
28
|
export function emitPointCorePython(program: PointCoreProgram): string {
|
|
@@ -42,11 +48,7 @@ function emitDeclaration(declaration: PointCoreDeclaration): string[] {
|
|
|
42
48
|
const moduleName = declaration.from.replace(/^\.\//, "").replace(/-/g, "_");
|
|
43
49
|
return [`from ${toPythonModuleName(moduleName)} import ${declaration.names.join(", ")}`];
|
|
44
50
|
}
|
|
45
|
-
if (declaration.kind === "external")
|
|
46
|
-
const moduleName = declaration.from.replace(/^\.\//, "").replace(/-/g, "_");
|
|
47
|
-
const imported = declaration.importName ?? declaration.name;
|
|
48
|
-
return [`from ${toPythonModuleName(moduleName)} import ${imported} as ${declaration.name}`];
|
|
49
|
-
}
|
|
51
|
+
if (declaration.kind === "external") return emitExternal(declaration);
|
|
50
52
|
if (declaration.kind === "type") return emitType(declaration);
|
|
51
53
|
if (declaration.kind === "value") return [emitValue(declaration)];
|
|
52
54
|
if (declaration.semantic && UNSUPPORTED_SEMANTIC_KINDS.has(declaration.semantic.kind)) {
|
|
@@ -62,6 +64,19 @@ function emitType(declaration: PointCoreTypeDeclaration): string[] {
|
|
|
62
64
|
];
|
|
63
65
|
}
|
|
64
66
|
|
|
67
|
+
function emitExternal(declaration: PointCoreExternalDeclaration): string[] {
|
|
68
|
+
if (declaration.from === "node:fs" && declaration.importName === "readFileSync") {
|
|
69
|
+
return [
|
|
70
|
+
`def ${declaration.name}(${declaration.params.map(emitParam).join(", ")}) -> ${emitTypeExpression(declaration.returnType)}:`,
|
|
71
|
+
" from pathlib import Path",
|
|
72
|
+
" return Path(path).read_text()",
|
|
73
|
+
];
|
|
74
|
+
}
|
|
75
|
+
const moduleName = declaration.from.replace(/^\.\//, "").replace(/-/g, "_");
|
|
76
|
+
const imported = declaration.importName ?? declaration.name;
|
|
77
|
+
return [`from ${toPythonModuleName(moduleName)} import ${imported} as ${declaration.name}`];
|
|
78
|
+
}
|
|
79
|
+
|
|
65
80
|
function emitFunction(declaration: PointCoreFunctionDeclaration): string[] {
|
|
66
81
|
const asyncPrefix = declaration.semantic?.kind === "action" || declaration.semantic?.kind === "workflow" || declaration.semantic?.kind === "command" ? "async " : "";
|
|
67
82
|
return [
|
|
@@ -79,7 +94,7 @@ function emitReturnType(declaration: PointCoreFunctionDeclaration): string {
|
|
|
79
94
|
|
|
80
95
|
function emitStatement(statement: PointCoreStatement, semanticKind?: string): string[] {
|
|
81
96
|
if (statement.kind === "return") {
|
|
82
|
-
if (semanticKind === "view") return ["# Point: view return values are not supported in Python emit yet", "return \"\""];
|
|
97
|
+
if (semanticKind === "view" || semanticKind === "page") return ["# Point: view/page return values are not supported in Python emit yet", "return \"\""];
|
|
83
98
|
return [statement.value ? `return ${emitExpression(statement.value)}` : "return"];
|
|
84
99
|
}
|
|
85
100
|
if (statement.kind === "value") return [emitValue(statement)];
|
|
@@ -9,6 +9,9 @@ import type {
|
|
|
9
9
|
PointCoreTypeDeclaration,
|
|
10
10
|
PointCoreTypeExpression,
|
|
11
11
|
PointCoreValueDeclaration,
|
|
12
|
+
PointSemanticPageLayout,
|
|
13
|
+
PointSemanticViewCheckboxBinding,
|
|
14
|
+
PointSemanticViewControls,
|
|
12
15
|
} from "./ast.ts";
|
|
13
16
|
|
|
14
17
|
const BINARY_OPERATORS: Record<string, string> = {
|
|
@@ -54,18 +57,86 @@ function emitFunction(declaration: PointCoreFunctionDeclaration): string[] {
|
|
|
54
57
|
const returnType =
|
|
55
58
|
declaration.semantic?.kind === "action" || declaration.semantic?.kind === "workflow" || declaration.semantic?.kind === "command"
|
|
56
59
|
? `Promise<${emitTypeExpression(declaration.returnType)}>`
|
|
57
|
-
: declaration.semantic?.kind === "view"
|
|
60
|
+
: declaration.semantic?.kind === "view" || declaration.semantic?.kind === "page"
|
|
58
61
|
? "JSX.Element"
|
|
59
62
|
: declaration.semantic?.kind === "route"
|
|
60
63
|
? "Response | string"
|
|
61
64
|
: emitTypeExpression(declaration.returnType);
|
|
65
|
+
const viewControls = declaration.semantic?.viewControls;
|
|
66
|
+
const bodyLines =
|
|
67
|
+
declaration.semantic?.kind === "page" && declaration.semantic.pageLayout
|
|
68
|
+
? emitPageBody(declaration.semantic.pageLayout)
|
|
69
|
+
: declaration.semantic?.kind === "view" && viewControls
|
|
70
|
+
? emitControlledViewBody(declaration.body, viewControls)
|
|
71
|
+
: declaration.body.flatMap((statement) => emitStatement(statement, declaration.semantic?.kind));
|
|
62
72
|
return [
|
|
63
73
|
`export ${asyncPrefix}function ${declaration.name}(${declaration.params.map(emitParam).join(", ")}): ${returnType} {`,
|
|
64
|
-
...indentLines(
|
|
74
|
+
...indentLines(bodyLines),
|
|
65
75
|
"}",
|
|
66
76
|
];
|
|
67
77
|
}
|
|
68
78
|
|
|
79
|
+
function emitControlledViewBody(body: PointCoreStatement[], controls: PointSemanticViewControls): string[] {
|
|
80
|
+
const checkboxLines = controls.checkboxes.map((binding) => emitCheckboxControl(binding, controls.changeCallback));
|
|
81
|
+
const contentJsx = emitViewContentExpression(body);
|
|
82
|
+
return ["return (", " <>", ...indentLines(checkboxLines), ` ${contentJsx}`, " </>", ");"];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function emitCheckboxControl(binding: PointSemanticViewCheckboxBinding, changeCallback: string): string {
|
|
86
|
+
const checked = emitExpression(binding.target);
|
|
87
|
+
return `<label><input type="checkbox" checked={${checked}} onChange={(e) => ${changeCallback}({ ...${binding.recordParam}, ${binding.fieldName}: e.target.checked })} />${escapeJsxText(binding.label)}</label>`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function emitViewContentExpression(body: PointCoreStatement[]): string {
|
|
91
|
+
let expression = "null";
|
|
92
|
+
for (let index = body.length - 1; index >= 0; index -= 1) {
|
|
93
|
+
const statement = body[index];
|
|
94
|
+
if (!statement) continue;
|
|
95
|
+
if (statement.kind === "return" && statement.value) {
|
|
96
|
+
expression = emitViewRenderFragment(statement.value);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (statement.kind === "if" && statement.thenBody.length === 1 && statement.thenBody[0]?.kind === "return" && statement.thenBody[0].value) {
|
|
100
|
+
const thenValue = emitViewRenderFragment(statement.thenBody[0].value);
|
|
101
|
+
expression = `${emitCondition(statement.condition)} ? ${thenValue} : ${expression}`;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return `{${expression}}`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function emitViewRenderFragment(expression: PointCoreExpression): string {
|
|
108
|
+
if (expression.kind === "literal" && typeof expression.value === "string") {
|
|
109
|
+
return `<>${escapeJsxText(expression.value)}</>`;
|
|
110
|
+
}
|
|
111
|
+
return `<>{${emitExpression(expression)}}</>`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function emitPageBody(layout: PointSemanticPageLayout): string[] {
|
|
115
|
+
const title = emitJsxChild(layout.title);
|
|
116
|
+
const description = layout.description ? `\n <p className="point-page-description">${emitJsxChild(layout.description)}</p>` : "";
|
|
117
|
+
const main = emitJsxChild(layout.main, true);
|
|
118
|
+
return [
|
|
119
|
+
`return (`,
|
|
120
|
+
` <main className="point-page">`,
|
|
121
|
+
` <header className="point-page-header">`,
|
|
122
|
+
` <h1>${title}</h1>${description}`,
|
|
123
|
+
` </header>`,
|
|
124
|
+
` <section className="point-page-main">${main}</section>`,
|
|
125
|
+
` </main>`,
|
|
126
|
+
`);`,
|
|
127
|
+
];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function emitJsxChild(expression: PointCoreExpression, allowComponent = false): string {
|
|
131
|
+
if (expression.kind === "literal" && typeof expression.value === "string") {
|
|
132
|
+
return escapeJsxText(expression.value);
|
|
133
|
+
}
|
|
134
|
+
if (allowComponent) {
|
|
135
|
+
return `{${emitExpression(expression)}}`;
|
|
136
|
+
}
|
|
137
|
+
return `{${emitExpression(expression)}}`;
|
|
138
|
+
}
|
|
139
|
+
|
|
69
140
|
function emitStatement(statement: PointCoreStatement, semanticKind?: string): string[] {
|
|
70
141
|
if (statement.kind === "return") {
|
|
71
142
|
if (semanticKind === "view" && statement.value) {
|
|
@@ -114,10 +185,20 @@ function emitValue(declaration: PointCoreValueDeclaration, exported: boolean): s
|
|
|
114
185
|
}
|
|
115
186
|
|
|
116
187
|
function emitParam(param: PointCoreParameter): string {
|
|
117
|
-
return `${param.name}: ${
|
|
188
|
+
return `${param.name}: ${emitParamType(param.type)}`;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function emitParamType(type: PointCoreTypeExpression): string {
|
|
192
|
+
if (type.name === "Handler" && type.args.length === 1) {
|
|
193
|
+
return `(value: ${emitTypeExpression(type.args[0]!)}) => void`;
|
|
194
|
+
}
|
|
195
|
+
return emitTypeExpression(type);
|
|
118
196
|
}
|
|
119
197
|
|
|
120
198
|
function emitTypeExpression(type: PointCoreTypeExpression): string {
|
|
199
|
+
if (type.name === "Handler" && type.args.length === 1) {
|
|
200
|
+
return `(value: ${emitTypeExpression(type.args[0]!)}) => void`;
|
|
201
|
+
}
|
|
121
202
|
if (type.name === "List") return `Array<${type.args[0] ? emitTypeExpression(type.args[0]) : "unknown"}>`;
|
|
122
203
|
if (type.name === "Maybe") return `${type.args[0] ? emitTypeExpression(type.args[0]) : "unknown"} | null`;
|
|
123
204
|
if (type.name === "Or") return type.args.map(emitTypeExpression).join(" | ");
|
package/src/core/index.ts
CHANGED
|
@@ -7,6 +7,7 @@ export * from "./emit-typescript.ts";
|
|
|
7
7
|
export * from "./emit-javascript.ts";
|
|
8
8
|
export * from "./emit-python.ts";
|
|
9
9
|
export * from "./incremental.ts";
|
|
10
|
+
export * from "./packages.ts";
|
|
10
11
|
export * from "./serialize.ts";
|
|
11
12
|
export * from "../semantic/index.ts";
|
|
12
13
|
export * from "./format.ts";
|