@hatchingpoint/point 0.0.12 → 0.0.13
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 +4 -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 +42 -13
- 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 +138 -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/http.ts +30 -0
- package/src/std/json.ts +13 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hatchingpoint/point",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.13",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Point language compiler and CLI.",
|
|
@@ -24,7 +24,9 @@
|
|
|
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"
|
|
28
30
|
},
|
|
29
31
|
"publishConfig": {
|
|
30
32
|
"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,12 @@ 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
10
|
import { formatPointSource } from "./format.ts";
|
|
11
11
|
import { isCacheHit, isIncrementalEnabled, readBuildCache, recordCacheEntry, writeBuildCache } from "./incremental.ts";
|
|
12
12
|
import { parsePointSource } from "./parser.ts";
|
|
13
13
|
import { runCheckDocs } from "./check-docs.ts";
|
|
14
|
+
import { addPointDependency, modulePathFromLock, POINT_LOCK, POINT_MANIFEST, readPointLock } from "./packages.ts";
|
|
14
15
|
import { runPointLspServer } from "../lsp/server.ts";
|
|
15
16
|
|
|
16
17
|
const DEFAULT_INPUT = "examples/math.point";
|
|
@@ -43,6 +44,18 @@ export async function main() {
|
|
|
43
44
|
return;
|
|
44
45
|
}
|
|
45
46
|
|
|
47
|
+
if (command === "add") {
|
|
48
|
+
const dependencyName = input;
|
|
49
|
+
const spec = output;
|
|
50
|
+
if (!dependencyName || !spec) {
|
|
51
|
+
throw new Error("Usage: point add <name> <spec> (spec: workspace:<path> | file:<path> | npm:<package>)");
|
|
52
|
+
}
|
|
53
|
+
const { manifest, lock } = await addPointDependency(dependencyName, spec);
|
|
54
|
+
console.log(`Point add updated ${POINT_MANIFEST} and ${POINT_LOCK}: ${dependencyName} -> ${spec}`);
|
|
55
|
+
console.log(JSON.stringify({ name: manifest.name, dependencies: manifest.dependencies, lockPackages: Object.keys(lock.packages) }, null, 2));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
46
59
|
const inputPath = resolve(process.cwd(), input);
|
|
47
60
|
const source = await Bun.file(inputPath).text();
|
|
48
61
|
const program = parsePointSource(source);
|
|
@@ -198,8 +211,9 @@ export async function main() {
|
|
|
198
211
|
async function runProjectCommand(command: string) {
|
|
199
212
|
const inputs = await discoverInputs();
|
|
200
213
|
if (inputs.length === 0) throw new Error(`No Point core files matched ${DEFAULT_PATTERNS.join(", ")}`);
|
|
201
|
-
const
|
|
202
|
-
const
|
|
214
|
+
const lock = await readPointLock();
|
|
215
|
+
const results = await Promise.all(inputs.map((input) => loadCoreFile(input, lock)));
|
|
216
|
+
const graph = createModuleGraph(results, lock);
|
|
203
217
|
const orderedResults = orderByDependencies(results, graph);
|
|
204
218
|
|
|
205
219
|
if (command === "fmt-all") {
|
|
@@ -298,6 +312,26 @@ async function runProjectCommand(command: string) {
|
|
|
298
312
|
return;
|
|
299
313
|
}
|
|
300
314
|
|
|
315
|
+
if (command === "build-py-all") {
|
|
316
|
+
const diagnostics = orderedResults.flatMap((result) =>
|
|
317
|
+
checkPointCore(programWithDependencyDeclarations(result, graph)).map((diagnostic) => ({ ...diagnostic, file: result.input })),
|
|
318
|
+
);
|
|
319
|
+
if (diagnostics.length > 0) {
|
|
320
|
+
console.error(JSON.stringify({ ok: false, diagnostics }, null, 2));
|
|
321
|
+
process.exit(1);
|
|
322
|
+
}
|
|
323
|
+
const pureLogicResults = orderedResults.filter((result) => isPureLogicProgram(result.program));
|
|
324
|
+
for (const result of pureLogicResults) {
|
|
325
|
+
const output = pyOutputFor(result.input);
|
|
326
|
+
const outputPath = resolve(process.cwd(), output);
|
|
327
|
+
await Bun.$`mkdir -p ${dirname(outputPath)}`.quiet();
|
|
328
|
+
await Bun.write(outputPath, emitPointCorePython(programWithTypeScriptImports(result, graph)));
|
|
329
|
+
}
|
|
330
|
+
const skipped = results.length - pureLogicResults.length;
|
|
331
|
+
console.log(`Point core Python build wrote ${pureLogicResults.length} files${skipped ? ` (${skipped} skipped)` : ""}`);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
|
|
301
335
|
if (command === "test-all") {
|
|
302
336
|
const diagnostics = orderedResults.flatMap((result) =>
|
|
303
337
|
checkPointCore(programWithDependencyDeclarations(result, graph)).map((diagnostic) => ({ ...diagnostic, file: result.input })),
|
|
@@ -409,9 +443,9 @@ export function findRunEntryName(program: PointCoreProgram): string | null {
|
|
|
409
443
|
return preferred?.name ?? null;
|
|
410
444
|
}
|
|
411
445
|
|
|
412
|
-
async function loadCoreFile(input: string) {
|
|
446
|
+
async function loadCoreFile(input: string, lock: Awaited<ReturnType<typeof readPointLock>>) {
|
|
413
447
|
const source = await Bun.file(resolve(process.cwd(), input)).text();
|
|
414
|
-
return { input, source, program: parsePointSource(source), uses: parseUseDeclarations(source, input) };
|
|
448
|
+
return { input, source, program: parsePointSource(source), uses: parseUseDeclarations(source, input, lock) };
|
|
415
449
|
}
|
|
416
450
|
|
|
417
451
|
type CoreFile = Awaited<ReturnType<typeof loadCoreFile>>;
|
|
@@ -423,15 +457,15 @@ interface UseDeclaration {
|
|
|
423
457
|
input: string;
|
|
424
458
|
}
|
|
425
459
|
|
|
426
|
-
function parseUseDeclarations(source: string, input: string): UseDeclaration[] {
|
|
460
|
+
function parseUseDeclarations(source: string, input: string, lock: Awaited<ReturnType<typeof readPointLock>>): UseDeclaration[] {
|
|
427
461
|
return source
|
|
428
462
|
.split(/\r?\n/)
|
|
429
463
|
.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
464
|
.filter((match): match is RegExpMatchArray => Boolean(match))
|
|
431
|
-
.map((match) => ({ moduleName: match[1]!, from: match[2] ??
|
|
465
|
+
.map((match) => ({ moduleName: match[1]!, from: match[2] ?? modulePathFromLock(lock, match[1]!), input }));
|
|
432
466
|
}
|
|
433
467
|
|
|
434
|
-
function createModuleGraph(results: CoreFile[]): ModuleGraph {
|
|
468
|
+
function createModuleGraph(results: CoreFile[], lock: Awaited<ReturnType<typeof readPointLock>>): ModuleGraph {
|
|
435
469
|
const byInput = new Map(results.map((result) => [normalizeInput(result.input), result]));
|
|
436
470
|
const graph: ModuleGraph = new Map();
|
|
437
471
|
for (const result of results) {
|
|
@@ -495,11 +529,6 @@ function resolveDependencyInput(input: string, from: string): string {
|
|
|
495
529
|
return resolve(base, from).replace(resolve(process.cwd()), "").replace(/^[/\\]/, "");
|
|
496
530
|
}
|
|
497
531
|
|
|
498
|
-
function stdPathFor(moduleName: string): string {
|
|
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`;
|
|
501
|
-
}
|
|
502
|
-
|
|
503
532
|
function normalizeInput(input: string): string {
|
|
504
533
|
return input.replaceAll("\\", "/");
|
|
505
534
|
}
|
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";
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join, relative, resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
export const POINT_MANIFEST = "point.json";
|
|
5
|
+
export const POINT_LOCK = "point.lock";
|
|
6
|
+
export const LOCK_SCHEMA = "point.lock.v1";
|
|
7
|
+
|
|
8
|
+
export interface PointManifest {
|
|
9
|
+
name: string;
|
|
10
|
+
version: string;
|
|
11
|
+
dependencies?: Record<string, string>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface PointLock {
|
|
15
|
+
schemaVersion: typeof LOCK_SCHEMA;
|
|
16
|
+
packages: Record<string, PointLockPackage>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface PointLockPackage {
|
|
20
|
+
version: string;
|
|
21
|
+
path?: string;
|
|
22
|
+
dependencies?: Record<string, string>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type DependencySpecKind = "workspace" | "file" | "npm";
|
|
26
|
+
|
|
27
|
+
export interface ParsedDependencySpec {
|
|
28
|
+
kind: DependencySpecKind;
|
|
29
|
+
locator: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function parseDependencySpec(spec: string): ParsedDependencySpec {
|
|
33
|
+
if (spec.startsWith("workspace:")) return { kind: "workspace", locator: spec.slice("workspace:".length) };
|
|
34
|
+
if (spec.startsWith("file:")) return { kind: "file", locator: spec.slice("file:".length) };
|
|
35
|
+
if (spec.startsWith("npm:")) return { kind: "npm", locator: spec.slice("npm:".length) };
|
|
36
|
+
throw new Error(`Invalid dependency spec "${spec}". Use workspace:<path>, file:<path>, or npm:<package> (npm not yet supported).`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function npmDependencyNotSupportedMessage(spec: string): string {
|
|
40
|
+
return `npm: registry dependencies are not supported yet (${spec}). Use workspace:<path> or file:<path> for local Point packages.`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function readPointManifest(cwd = process.cwd()): Promise<PointManifest> {
|
|
44
|
+
const path = join(cwd, POINT_MANIFEST);
|
|
45
|
+
if (!existsSync(path)) throw new Error(`Missing ${POINT_MANIFEST} in ${cwd}`);
|
|
46
|
+
return Bun.file(path).json() as Promise<PointManifest>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function writePointManifest(manifest: PointManifest, cwd = process.cwd()): Promise<void> {
|
|
50
|
+
const path = join(cwd, POINT_MANIFEST);
|
|
51
|
+
await Bun.write(path, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function readPointLock(cwd = process.cwd()): Promise<PointLock | null> {
|
|
55
|
+
const path = join(cwd, POINT_LOCK);
|
|
56
|
+
if (!existsSync(path)) return null;
|
|
57
|
+
const lock = (await Bun.file(path).json()) as PointLock;
|
|
58
|
+
if (lock.schemaVersion !== LOCK_SCHEMA) {
|
|
59
|
+
throw new Error(`Unsupported ${POINT_LOCK} schema: ${lock.schemaVersion}`);
|
|
60
|
+
}
|
|
61
|
+
return lock;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function writePointLock(lock: PointLock, cwd = process.cwd()): Promise<void> {
|
|
65
|
+
const path = join(cwd, POINT_LOCK);
|
|
66
|
+
await Bun.write(path, `${JSON.stringify(lock, null, 2)}\n`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function normalizePackagePath(cwd: string, rawPath: string): string {
|
|
70
|
+
const absolute = resolve(cwd, rawPath);
|
|
71
|
+
if (!existsSync(absolute)) {
|
|
72
|
+
throw new Error(`Dependency path does not exist: ${rawPath}`);
|
|
73
|
+
}
|
|
74
|
+
return relative(cwd, absolute).split("\\").join("/") || ".";
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function resolveDependencySpec(spec: string, cwd = process.cwd()): PointLockPackage {
|
|
78
|
+
const parsed = parseDependencySpec(spec);
|
|
79
|
+
if (parsed.kind === "npm") throw new Error(npmDependencyNotSupportedMessage(spec));
|
|
80
|
+
const path = normalizePackagePath(cwd, parsed.locator);
|
|
81
|
+
return { version: parsed.kind, path };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function resolveLockFromManifest(manifest: PointManifest, cwd = process.cwd()): Promise<PointLock> {
|
|
85
|
+
const packages: Record<string, PointLockPackage> = {
|
|
86
|
+
[manifest.name]: {
|
|
87
|
+
version: manifest.version,
|
|
88
|
+
dependencies: manifest.dependencies ?? {},
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
for (const [name, spec] of Object.entries(manifest.dependencies ?? {})) {
|
|
92
|
+
packages[name] = resolveDependencySpec(spec, cwd);
|
|
93
|
+
const entry = packages[name];
|
|
94
|
+
if (!entry.path) continue;
|
|
95
|
+
const nestedManifestPath = join(cwd, entry.path, POINT_MANIFEST);
|
|
96
|
+
if (!existsSync(nestedManifestPath)) continue;
|
|
97
|
+
const nested = (await Bun.file(nestedManifestPath).json()) as PointManifest;
|
|
98
|
+
packages[nested.name] = {
|
|
99
|
+
version: nested.version,
|
|
100
|
+
dependencies: nested.dependencies ?? {},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
return { schemaVersion: LOCK_SCHEMA, packages };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export async function addPointDependency(name: string, spec: string, cwd = process.cwd()): Promise<{ manifest: PointManifest; lock: PointLock }> {
|
|
107
|
+
if (!name || !/^[A-Za-z][A-Za-z0-9_-]*$/.test(name)) {
|
|
108
|
+
throw new Error(`Invalid dependency name "${name}". Use an identifier like std or point-logic.`);
|
|
109
|
+
}
|
|
110
|
+
parseDependencySpec(spec);
|
|
111
|
+
const manifest = await readPointManifest(cwd);
|
|
112
|
+
manifest.dependencies = { ...(manifest.dependencies ?? {}), [name]: spec };
|
|
113
|
+
const lock = await resolveLockFromManifest(manifest, cwd);
|
|
114
|
+
await writePointManifest(manifest, cwd);
|
|
115
|
+
await writePointLock(lock, cwd);
|
|
116
|
+
return { manifest, lock };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function packageRootFromLock(lock: PointLock | null, packageName: string): string | null {
|
|
120
|
+
const entry = lock?.packages[packageName];
|
|
121
|
+
if (entry?.path) return entry.path;
|
|
122
|
+
if (!lock && packageName === "std") return "std";
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function modulePathFromLock(lock: PointLock | null, moduleName: string): string {
|
|
127
|
+
const dot = moduleName.indexOf(".");
|
|
128
|
+
if (dot < 0) {
|
|
129
|
+
throw new Error(`Use declarations without from must target package modules: ${moduleName}`);
|
|
130
|
+
}
|
|
131
|
+
const packageName = moduleName.slice(0, dot);
|
|
132
|
+
const modulePath = moduleName.slice(dot + 1);
|
|
133
|
+
const root = packageRootFromLock(lock, packageName);
|
|
134
|
+
if (!root) {
|
|
135
|
+
throw new Error(`Unknown package "${packageName}" in ${moduleName}. Add it with: point add ${packageName} <spec>`);
|
|
136
|
+
}
|
|
137
|
+
return `${root}/${modulePath.replaceAll(".", "/")}.point`;
|
|
138
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export function isSemanticPointSyntax(source: string): boolean {
|
|
2
2
|
return source
|
|
3
3
|
.split(/\r?\n/)
|
|
4
|
-
.some((line) => /^(use|record|calculation|rule|label|external|action|policy|view|route|workflow|command)\s+/.test(line.trim()));
|
|
4
|
+
.some((line) => /^(use|record|calculation|rule|label|external|action|policy|view|page|route|workflow|command)\s+/.test(line.trim()));
|
|
5
5
|
}
|
|
6
6
|
|
|
7
7
|
export function assertSemanticPointSource(source: string) {
|
|
@@ -12,7 +12,7 @@ export function assertSemanticPointSource(source: string) {
|
|
|
12
12
|
for (const [index, line] of lines.entries()) {
|
|
13
13
|
const trimmed = line.trim();
|
|
14
14
|
if (!trimmed || trimmed.startsWith("//")) continue;
|
|
15
|
-
if (/^(record|calculation|rule|label|external|action|policy|view|route|workflow|command)\s+/.test(trimmed)) hasSemanticDeclaration = true;
|
|
15
|
+
if (/^(record|calculation|rule|label|external|action|policy|view|page|route|workflow|command)\s+/.test(trimmed)) hasSemanticDeclaration = true;
|
|
16
16
|
if (oldStyleTopLevel.test(trimmed)) {
|
|
17
17
|
throw new Error(
|
|
18
18
|
`Point source uses internal core syntax at ${index + 1}:1. Use record, calculation, rule, or label instead.`,
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { PointCoreProgram } from "../ast.ts";
|
|
2
2
|
import { assertSemanticPointSource, isSemanticPointSyntax } from "../semantic-source.ts";
|
|
3
|
+
import { desugarSemanticProgram } from "../../semantic/desugar.ts";
|
|
4
|
+
import { parseSemanticSource } from "../../semantic/parse.ts";
|
|
3
5
|
import { parsePointCore } from "./core-text-parser.ts";
|
|
4
6
|
|
|
5
7
|
/** Legacy string-lowering path retained for migration parity tests only. */
|
|
@@ -11,10 +13,27 @@ export function parsePointSourceLegacy(source: string): PointCoreProgram {
|
|
|
11
13
|
function parsePointSourceViaLowering(source: string): PointCoreProgram {
|
|
12
14
|
const lowered = lowerSemanticPointSyntax(source);
|
|
13
15
|
const program = parsePointCore(lowered);
|
|
14
|
-
if (isSemanticPointSyntax(source))
|
|
16
|
+
if (isSemanticPointSyntax(source)) {
|
|
17
|
+
attachSemanticMetadata(program, source);
|
|
18
|
+
enrichPageLayouts(program, source);
|
|
19
|
+
}
|
|
15
20
|
return program;
|
|
16
21
|
}
|
|
17
22
|
|
|
23
|
+
function enrichPageLayouts(program: PointCoreProgram, source: string): void {
|
|
24
|
+
const desugared = desugarSemanticProgram(parseSemanticSource(source));
|
|
25
|
+
const pageFunctions = desugared.declarations.filter(
|
|
26
|
+
(declaration): declaration is Extract<(typeof desugared.declarations)[number], { kind: "function" }> =>
|
|
27
|
+
declaration.kind === "function" && declaration.semantic?.kind === "page",
|
|
28
|
+
);
|
|
29
|
+
for (const declaration of program.declarations) {
|
|
30
|
+
if (declaration.kind !== "function" || declaration.semantic?.kind !== "page") continue;
|
|
31
|
+
const match = pageFunctions.find((candidate) => candidate.name === declaration.name);
|
|
32
|
+
if (!match?.semantic?.pageLayout) continue;
|
|
33
|
+
declaration.semantic.pageLayout = match.semantic.pageLayout;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
18
37
|
function lowerSemanticPointSyntax(source: string): string {
|
|
19
38
|
if (!isSemanticPointSyntax(source)) return source;
|
|
20
39
|
const lines = source.split(/\r?\n/);
|
|
@@ -87,6 +106,12 @@ function lowerSemanticPointSyntax(source: string): string {
|
|
|
87
106
|
index = lowered.next;
|
|
88
107
|
continue;
|
|
89
108
|
}
|
|
109
|
+
if (trimmed.startsWith("page ")) {
|
|
110
|
+
const lowered = lowerPage(lines, index, records, externalBindings);
|
|
111
|
+
output.push(...lowered.lines);
|
|
112
|
+
index = lowered.next;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
90
115
|
if (trimmed.startsWith("route ")) {
|
|
91
116
|
const lowered = lowerRoute(lines, index, records, externalBindings);
|
|
92
117
|
output.push(...lowered.lines);
|
|
@@ -120,7 +145,7 @@ function assertSemanticPointSource(source: string) {
|
|
|
120
145
|
for (const [index, line] of lines.entries()) {
|
|
121
146
|
const trimmed = line.trim();
|
|
122
147
|
if (!trimmed || trimmed.startsWith("//")) continue;
|
|
123
|
-
if (/^(record|calculation|rule|label|external|action|policy|view|route|workflow|command)\s+/.test(trimmed)) hasSemanticDeclaration = true;
|
|
148
|
+
if (/^(record|calculation|rule|label|external|action|policy|view|page|route|workflow|command)\s+/.test(trimmed)) hasSemanticDeclaration = true;
|
|
124
149
|
if (oldStyleTopLevel.test(trimmed)) {
|
|
125
150
|
throw new Error(
|
|
126
151
|
`Point source uses internal core syntax at ${index + 1}:1. Use record, calculation, rule, or label instead.`,
|
|
@@ -534,6 +559,44 @@ function lowerView(
|
|
|
534
559
|
};
|
|
535
560
|
}
|
|
536
561
|
|
|
562
|
+
function lowerPage(
|
|
563
|
+
lines: string[],
|
|
564
|
+
start: number,
|
|
565
|
+
records: Map<string, Map<string, string>>,
|
|
566
|
+
externalBindings: Map<string, string> = new Map(),
|
|
567
|
+
): { lines: string[]; next: number } {
|
|
568
|
+
const label = (lines[start] ?? "").trim().slice("page ".length).trim();
|
|
569
|
+
const body = collectSemanticBody(lines, start + 1);
|
|
570
|
+
const params: string[] = [];
|
|
571
|
+
const paramTypes = new Map<string, string>();
|
|
572
|
+
const bindings = new Map<string, string>(externalBindings);
|
|
573
|
+
let mainExpression: string | undefined;
|
|
574
|
+
|
|
575
|
+
for (const line of body.lines) {
|
|
576
|
+
if (line.startsWith("input ")) {
|
|
577
|
+
const param = parseTypedBinding(line.slice("input ".length));
|
|
578
|
+
const paramName = toIdentifier(param.name);
|
|
579
|
+
params.push(`${paramName}: ${param.type}`);
|
|
580
|
+
paramTypes.set(paramName, param.type);
|
|
581
|
+
bindings.set(param.name, paramName);
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
584
|
+
if (line.startsWith("title ") || line.startsWith("description ")) continue;
|
|
585
|
+
if (line.startsWith("main render ")) {
|
|
586
|
+
mainExpression = lowerExpression(line.slice("main render ".length), paramTypes, records, bindings);
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
throw new Error(`Unknown page statement: ${line}`);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
if (!mainExpression) throw new Error(`Page ${label} requires main render`);
|
|
593
|
+
|
|
594
|
+
return {
|
|
595
|
+
lines: [`fn ${semanticFunctionName(label, "page", "page")}(${params.join(", ")}): Text {`, ` return ${mainExpression}`, "}", ""],
|
|
596
|
+
next: body.next,
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
|
|
537
600
|
function lowerRoute(
|
|
538
601
|
lines: string[],
|
|
539
602
|
start: number,
|
|
@@ -745,6 +808,7 @@ function collectSemanticDeclarationInfo(source: string): SemanticDeclarationInfo
|
|
|
745
808
|
trimmed.startsWith("action ") ||
|
|
746
809
|
trimmed.startsWith("policy ") ||
|
|
747
810
|
trimmed.startsWith("view ") ||
|
|
811
|
+
trimmed.startsWith("page ") ||
|
|
748
812
|
trimmed.startsWith("route ") ||
|
|
749
813
|
trimmed.startsWith("workflow ") ||
|
|
750
814
|
trimmed.startsWith("command ")
|
|
@@ -761,7 +825,9 @@ function collectSemanticDeclarationInfo(source: string): SemanticDeclarationInfo
|
|
|
761
825
|
? "policy"
|
|
762
826
|
: trimmed.startsWith("view ")
|
|
763
827
|
? "view"
|
|
764
|
-
: trimmed.startsWith("
|
|
828
|
+
: trimmed.startsWith("page ")
|
|
829
|
+
? "page"
|
|
830
|
+
: trimmed.startsWith("route ")
|
|
765
831
|
? "route"
|
|
766
832
|
: trimmed.startsWith("workflow ")
|
|
767
833
|
? "workflow"
|
|
@@ -771,7 +837,7 @@ function collectSemanticDeclarationInfo(source: string): SemanticDeclarationInfo
|
|
|
771
837
|
const body = collectSemanticBody(lines, index + 1);
|
|
772
838
|
const params = new Map<string, string>();
|
|
773
839
|
let outputName =
|
|
774
|
-
kind === "label" ? "label" : kind === "policy" ? "policy" : kind === "view" ? "view" : kind === "route" ? "route" : "result";
|
|
840
|
+
kind === "label" ? "label" : kind === "policy" ? "policy" : kind === "view" ? "view" : kind === "page" ? "page" : kind === "route" ? "route" : "result";
|
|
775
841
|
const effects: string[] = [];
|
|
776
842
|
for (const line of body.lines) {
|
|
777
843
|
if (line.startsWith("input ")) {
|
|
@@ -840,6 +906,7 @@ function collectCallableBindings(source: string): Map<string, string> {
|
|
|
840
906
|
declaration.kind === "action" ||
|
|
841
907
|
declaration.kind === "policy" ||
|
|
842
908
|
declaration.kind === "view" ||
|
|
909
|
+
declaration.kind === "page" ||
|
|
843
910
|
declaration.kind === "route" ||
|
|
844
911
|
declaration.kind === "workflow" ||
|
|
845
912
|
declaration.kind === "command"
|
|
@@ -870,7 +937,7 @@ function collectSemanticBody(lines: string[], start: number): { lines: string[];
|
|
|
870
937
|
}
|
|
871
938
|
|
|
872
939
|
function isSemanticTopLevel(line: string): boolean {
|
|
873
|
-
return /^(module|use|record|calculation|rule|label|external|action|policy|view|route|workflow|command|type|fn|let|var|import)\s+/.test(line);
|
|
940
|
+
return /^(module|use|record|calculation|rule|label|external|action|policy|view|page|route|workflow|command|type|fn|let|var|import)\s+/.test(line);
|
|
874
941
|
}
|
|
875
942
|
|
|
876
943
|
function isSemanticLoopBoundary(line: string): boolean {
|
|
@@ -973,7 +1040,7 @@ function toIdentifier(label: string): string {
|
|
|
973
1040
|
function semanticFunctionName(
|
|
974
1041
|
label: string,
|
|
975
1042
|
outputName: string,
|
|
976
|
-
kind: "calculation" | "rule" | "label" | "action" | "policy" | "view" | "route" | "workflow" | "command",
|
|
1043
|
+
kind: "calculation" | "rule" | "label" | "action" | "policy" | "view" | "page" | "route" | "workflow" | "command",
|
|
977
1044
|
): string {
|
|
978
1045
|
const base = toIdentifier(label);
|
|
979
1046
|
const suffix =
|
|
@@ -983,7 +1050,9 @@ function semanticFunctionName(
|
|
|
983
1050
|
? "Policy"
|
|
984
1051
|
: kind === "view"
|
|
985
1052
|
? "View"
|
|
986
|
-
: kind === "
|
|
1053
|
+
: kind === "page"
|
|
1054
|
+
? "Page"
|
|
1055
|
+
: kind === "route"
|
|
987
1056
|
? "Route"
|
|
988
1057
|
: kind === "workflow"
|
|
989
1058
|
? "Workflow"
|
package/src/semantic/ast.ts
CHANGED
|
@@ -24,6 +24,7 @@ export type PointSemanticDeclaration =
|
|
|
24
24
|
| PointSemanticActionDeclaration
|
|
25
25
|
| PointSemanticPolicyDeclaration
|
|
26
26
|
| PointSemanticViewDeclaration
|
|
27
|
+
| PointSemanticPageDeclaration
|
|
27
28
|
| PointSemanticRouteDeclaration
|
|
28
29
|
| PointSemanticWorkflowDeclaration
|
|
29
30
|
| PointSemanticCommandDeclaration;
|
|
@@ -123,6 +124,16 @@ export interface PointSemanticViewDeclaration {
|
|
|
123
124
|
span?: PointSourceSpan;
|
|
124
125
|
}
|
|
125
126
|
|
|
127
|
+
export interface PointSemanticPageDeclaration {
|
|
128
|
+
kind: "page";
|
|
129
|
+
name: string;
|
|
130
|
+
inputs: PointSemanticBinding[];
|
|
131
|
+
title: PointSemanticExpression;
|
|
132
|
+
description?: PointSemanticExpression;
|
|
133
|
+
main: PointSemanticExpression;
|
|
134
|
+
span?: PointSourceSpan;
|
|
135
|
+
}
|
|
136
|
+
|
|
126
137
|
export interface PointSemanticRouteDeclaration {
|
|
127
138
|
kind: "route";
|
|
128
139
|
name: string;
|
|
@@ -182,7 +193,9 @@ export type PointSemanticPolicyStatement =
|
|
|
182
193
|
|
|
183
194
|
export type PointSemanticViewStatement =
|
|
184
195
|
| { kind: "render"; value: PointSemanticExpression; span?: PointSourceSpan }
|
|
185
|
-
| { kind: "whenRender"; condition: PointSemanticExpression; value: PointSemanticExpression; span?: PointSourceSpan }
|
|
196
|
+
| { kind: "whenRender"; condition: PointSemanticExpression; value: PointSemanticExpression; span?: PointSourceSpan }
|
|
197
|
+
| { kind: "bindCheckbox"; label: string; target: PointSemanticExpression; span?: PointSourceSpan }
|
|
198
|
+
| { kind: "onChangeCall"; callback: string; span?: PointSourceSpan };
|
|
186
199
|
|
|
187
200
|
export type PointSemanticRouteStatement = { kind: "return"; value: PointSemanticExpression; span?: PointSourceSpan };
|
|
188
201
|
|
|
@@ -4,6 +4,7 @@ const CALLABLE_KEYWORDS = [
|
|
|
4
4
|
"label",
|
|
5
5
|
"action",
|
|
6
6
|
"view",
|
|
7
|
+
"page",
|
|
7
8
|
"route",
|
|
8
9
|
"workflow",
|
|
9
10
|
"command",
|
|
@@ -47,5 +48,5 @@ function collectBody(lines: string[], start: number): { lines: string[]; next: n
|
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
function isTopLevel(line: string): boolean {
|
|
50
|
-
return /^(module|use|record|calculation|rule|label|external|action|policy|view|route|workflow|command)\s+/.test(line);
|
|
51
|
+
return /^(module|use|record|calculation|rule|label|external|action|policy|view|page|route|workflow|command)\s+/.test(line);
|
|
51
52
|
}
|
package/src/semantic/context.ts
CHANGED
|
@@ -27,6 +27,7 @@ export type PointSemanticSymbolKind =
|
|
|
27
27
|
| "action"
|
|
28
28
|
| "policy"
|
|
29
29
|
| "view"
|
|
30
|
+
| "page"
|
|
30
31
|
| "route"
|
|
31
32
|
| "workflow"
|
|
32
33
|
| "command"
|
|
@@ -215,6 +216,15 @@ function callableDeclaration(declaration: PointSemanticDeclaration):
|
|
|
215
216
|
if (declaration.kind === "view" || declaration.kind === "route" || declaration.kind === "workflow" || declaration.kind === "command") {
|
|
216
217
|
return { kind: declaration.kind, name: declaration.name, inputs: declaration.inputs, output: declaration.output, declaration };
|
|
217
218
|
}
|
|
219
|
+
if (declaration.kind === "page") {
|
|
220
|
+
return {
|
|
221
|
+
kind: "page",
|
|
222
|
+
name: declaration.name,
|
|
223
|
+
inputs: declaration.inputs,
|
|
224
|
+
output: { name: "page", type: { kind: "typeRef", name: "Page", args: [] } },
|
|
225
|
+
declaration,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
218
228
|
return null;
|
|
219
229
|
}
|
|
220
230
|
|
|
@@ -313,7 +323,7 @@ function relatedRefsFor(symbol: PointSemanticSymbol, index: PointSemanticIndex):
|
|
|
313
323
|
const ownerPath = symbol.path.split(".").slice(0, 2).join(".");
|
|
314
324
|
return index.refs.filter((candidate) => candidate.path.startsWith(`${ownerPath}.`) && candidate.ref !== symbol.ref).map((candidate) => candidate.ref);
|
|
315
325
|
}
|
|
316
|
-
if (symbol.kind === "record" || symbol.kind === "calculation" || symbol.kind === "rule" || symbol.kind === "label" || symbol.kind === "action" || symbol.kind === "policy" || symbol.kind === "view" || symbol.kind === "route" || symbol.kind === "workflow" || symbol.kind === "command" || symbol.kind === "external") {
|
|
326
|
+
if (symbol.kind === "record" || symbol.kind === "calculation" || symbol.kind === "rule" || symbol.kind === "label" || symbol.kind === "action" || symbol.kind === "policy" || symbol.kind === "view" || symbol.kind === "page" || symbol.kind === "route" || symbol.kind === "workflow" || symbol.kind === "command" || symbol.kind === "external") {
|
|
317
327
|
return index.refs.filter((candidate) => candidate.path.startsWith(`${symbol.path}.`)).map((candidate) => candidate.ref);
|
|
318
328
|
}
|
|
319
329
|
return [];
|
|
@@ -332,6 +342,7 @@ function summaryFor(symbol: PointSemanticSymbol): string {
|
|
|
332
342
|
if (symbol.kind === "action") return `Semantic action ${symbol.name} returns ${symbol.type}; effects: ${(symbol.effects ?? []).join(", ") || "none"}.`;
|
|
333
343
|
if (symbol.kind === "policy") return `Semantic policy ${symbol.name} returns ${symbol.type}.`;
|
|
334
344
|
if (symbol.kind === "view") return `Semantic view ${symbol.name} returns ${symbol.type}.`;
|
|
345
|
+
if (symbol.kind === "page") return `Semantic page ${symbol.name} returns a Next.js page shell (${symbol.type}).`;
|
|
335
346
|
if (symbol.kind === "route") return `Semantic route ${symbol.name} returns ${symbol.type}.`;
|
|
336
347
|
if (symbol.kind === "workflow") return `Semantic workflow ${symbol.name} returns ${symbol.type}.`;
|
|
337
348
|
if (symbol.kind === "command") return `Semantic command ${symbol.name} returns ${symbol.type}.`;
|
package/src/semantic/desugar.ts
CHANGED
|
@@ -11,6 +11,7 @@ import type {
|
|
|
11
11
|
PointCoreTypeExpression,
|
|
12
12
|
PointCoreValueDeclaration,
|
|
13
13
|
PointSourceSpan,
|
|
14
|
+
PointSemanticViewControls,
|
|
14
15
|
} from "../core/ast.ts";
|
|
15
16
|
import type {
|
|
16
17
|
PointSemanticBinding,
|
|
@@ -35,6 +36,8 @@ import type {
|
|
|
35
36
|
PointSemanticTypeExpression,
|
|
36
37
|
PointSemanticUseDeclaration,
|
|
37
38
|
PointSemanticViewDeclaration,
|
|
39
|
+
PointSemanticViewStatement,
|
|
40
|
+
PointSemanticPageDeclaration,
|
|
38
41
|
PointSemanticWorkflowDeclaration,
|
|
39
42
|
PointSemanticWorkflowStatement,
|
|
40
43
|
PointSemanticActionDeclaration,
|
|
@@ -104,6 +107,7 @@ function buildCallableMap(
|
|
|
104
107
|
declaration.kind === "action" ||
|
|
105
108
|
declaration.kind === "policy" ||
|
|
106
109
|
declaration.kind === "view" ||
|
|
110
|
+
declaration.kind === "page" ||
|
|
107
111
|
declaration.kind === "route" ||
|
|
108
112
|
declaration.kind === "workflow" ||
|
|
109
113
|
declaration.kind === "command"
|
|
@@ -119,6 +123,7 @@ function defaultOutputName(declaration: PointSemanticDeclaration): string {
|
|
|
119
123
|
if (declaration.kind === "label") return "label";
|
|
120
124
|
if (declaration.kind === "policy") return "policy";
|
|
121
125
|
if (declaration.kind === "view") return "view";
|
|
126
|
+
if (declaration.kind === "page") return "page";
|
|
122
127
|
if (declaration.kind === "route") return "route";
|
|
123
128
|
if ("output" in declaration) return toIdentifier(declaration.output.name);
|
|
124
129
|
return "result";
|
|
@@ -152,6 +157,8 @@ function desugarDeclaration(
|
|
|
152
157
|
return [desugarPolicy(declaration, records, callables)];
|
|
153
158
|
case "view":
|
|
154
159
|
return [desugarView(declaration, records, callables)];
|
|
160
|
+
case "page":
|
|
161
|
+
return [desugarPage(declaration, records, callables)];
|
|
155
162
|
case "route":
|
|
156
163
|
return [desugarRoute(declaration, records, callables)];
|
|
157
164
|
case "workflow":
|
|
@@ -296,13 +303,85 @@ function desugarView(
|
|
|
296
303
|
const outputType: PointCoreTypeExpression = { kind: "typeRef", name: "Text", args: [] };
|
|
297
304
|
const { params, bindings } = collectBindings(declaration.inputs, declaration.output);
|
|
298
305
|
const ctx: DesugarContext = { records, callables, bindings, outputName: "page", outputType };
|
|
306
|
+
const renderStatements = declaration.body.filter(
|
|
307
|
+
(statement): statement is Extract<PointSemanticViewStatement, { kind: "render" | "whenRender" }> =>
|
|
308
|
+
statement.kind === "render" || statement.kind === "whenRender",
|
|
309
|
+
);
|
|
310
|
+
const metadata = semanticDeclarationMetadata(declaration);
|
|
311
|
+
const viewControls = buildViewControls(declaration, ctx);
|
|
312
|
+
if (viewControls) metadata.viewControls = viewControls;
|
|
299
313
|
return {
|
|
300
314
|
kind: "function",
|
|
301
315
|
name: semanticFunctionName(declaration.name, "view", "view"),
|
|
302
316
|
params,
|
|
303
317
|
returnType: outputType,
|
|
304
|
-
body: desugarViewBody(
|
|
305
|
-
semantic:
|
|
318
|
+
body: desugarViewBody(renderStatements, ctx),
|
|
319
|
+
semantic: metadata,
|
|
320
|
+
span: declaration.span,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function buildViewControls(
|
|
325
|
+
declaration: PointSemanticViewDeclaration,
|
|
326
|
+
ctx: DesugarContext,
|
|
327
|
+
): PointSemanticViewControls | undefined {
|
|
328
|
+
const bindStatements = declaration.body.filter(
|
|
329
|
+
(statement): statement is Extract<PointSemanticViewStatement, { kind: "bindCheckbox" }> => statement.kind === "bindCheckbox",
|
|
330
|
+
);
|
|
331
|
+
if (bindStatements.length === 0) return undefined;
|
|
332
|
+
|
|
333
|
+
const onChangeCall = declaration.body.find(
|
|
334
|
+
(statement): statement is Extract<PointSemanticViewStatement, { kind: "onChangeCall" }> => statement.kind === "onChangeCall",
|
|
335
|
+
);
|
|
336
|
+
const handlerInput = declaration.inputs.find((input) => input.type.name === "Handler" && input.type.args.length === 1);
|
|
337
|
+
const callbackLabel = onChangeCall?.callback ?? handlerInput?.label;
|
|
338
|
+
if (!callbackLabel) {
|
|
339
|
+
throw new Error(`View ${declaration.name} with bind checkbox requires input Handler T or on change call`);
|
|
340
|
+
}
|
|
341
|
+
const changeCallback = toIdentifier(callbackLabel);
|
|
342
|
+
|
|
343
|
+
return {
|
|
344
|
+
changeCallback,
|
|
345
|
+
checkboxes: bindStatements.map((statement) => {
|
|
346
|
+
const target = desugarExpression(statement.target, ctx);
|
|
347
|
+
if (target.kind !== "property") {
|
|
348
|
+
throw new Error(`bind checkbox target must be a record field access`);
|
|
349
|
+
}
|
|
350
|
+
if (target.target.kind !== "identifier") {
|
|
351
|
+
throw new Error(`bind checkbox target must start with an input record`);
|
|
352
|
+
}
|
|
353
|
+
return {
|
|
354
|
+
label: statement.label,
|
|
355
|
+
target,
|
|
356
|
+
recordParam: target.target.name,
|
|
357
|
+
fieldName: target.name,
|
|
358
|
+
};
|
|
359
|
+
}),
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function desugarPage(
|
|
364
|
+
declaration: PointSemanticPageDeclaration,
|
|
365
|
+
records: Map<string, Map<string, string>>,
|
|
366
|
+
callables: Map<string, string>,
|
|
367
|
+
): PointCoreFunctionDeclaration {
|
|
368
|
+
const outputType: PointCoreTypeExpression = { kind: "typeRef", name: "Text", args: [] };
|
|
369
|
+
const pageType: PointCoreTypeExpression = { kind: "typeRef", name: "Page", args: [] };
|
|
370
|
+
const { params, bindings } = collectBindings(declaration.inputs, { name: "page", type: pageType });
|
|
371
|
+
const ctx: DesugarContext = { records, callables, bindings, outputName: "page", outputType };
|
|
372
|
+
const metadata = semanticDeclarationMetadata(declaration);
|
|
373
|
+
metadata.pageLayout = {
|
|
374
|
+
title: desugarExpression(declaration.title, ctx),
|
|
375
|
+
description: declaration.description ? desugarExpression(declaration.description, ctx) : undefined,
|
|
376
|
+
main: desugarExpression(declaration.main, ctx),
|
|
377
|
+
};
|
|
378
|
+
return {
|
|
379
|
+
kind: "function",
|
|
380
|
+
name: semanticFunctionName(declaration.name, "page", "page"),
|
|
381
|
+
params,
|
|
382
|
+
returnType: outputType,
|
|
383
|
+
body: [{ kind: "return", value: metadata.pageLayout.main, span: declaration.span }],
|
|
384
|
+
semantic: metadata,
|
|
306
385
|
span: declaration.span,
|
|
307
386
|
};
|
|
308
387
|
}
|
|
@@ -390,7 +469,7 @@ function desugarParameter(binding: PointSemanticBinding): PointCoreParameter {
|
|
|
390
469
|
}
|
|
391
470
|
|
|
392
471
|
function desugarType(type: PointSemanticTypeExpression): PointCoreTypeExpression {
|
|
393
|
-
if (type.name === "List" || type.name === "Maybe" || type.name === "Or") {
|
|
472
|
+
if (type.name === "List" || type.name === "Maybe" || type.name === "Or" || type.name === "Handler") {
|
|
394
473
|
return { kind: "typeRef", name: type.name, args: type.args.map(desugarType) };
|
|
395
474
|
}
|
|
396
475
|
const primitives = new Set(["Text", "Int", "Float", "Bool", "Void", "Error", "Page"]);
|
|
@@ -32,6 +32,14 @@ export function parseSemanticTypeExpression(source: string): PointSemanticTypeEx
|
|
|
32
32
|
if (maybeMatch) {
|
|
33
33
|
return { kind: "typeRef", name: "Maybe", args: [parseSemanticTypeExpression(maybeMatch[1] ?? "")] };
|
|
34
34
|
}
|
|
35
|
+
const handlerMatch = trimmed.match(/^Handler\s+(.+)$/);
|
|
36
|
+
if (handlerMatch) {
|
|
37
|
+
return { kind: "typeRef", name: "Handler", args: [parseSemanticTypeExpression(handlerMatch[1] ?? "")] };
|
|
38
|
+
}
|
|
39
|
+
const handlerGenericMatch = trimmed.match(/^Handler<(.+)>$/);
|
|
40
|
+
if (handlerGenericMatch) {
|
|
41
|
+
return { kind: "typeRef", name: "Handler", args: [parseSemanticTypeExpression(handlerGenericMatch[1] ?? "")] };
|
|
42
|
+
}
|
|
35
43
|
return { kind: "typeRef", name: trimmed, args: [] };
|
|
36
44
|
}
|
|
37
45
|
|
package/src/semantic/format.ts
CHANGED
|
@@ -83,6 +83,14 @@ function formatDeclaration(declaration: PointSemanticDeclaration): string[] {
|
|
|
83
83
|
...formatViewOutput(declaration.output),
|
|
84
84
|
...declaration.body.map((statement) => ` ${formatViewStatement(statement)}`),
|
|
85
85
|
];
|
|
86
|
+
case "page":
|
|
87
|
+
return [
|
|
88
|
+
`page ${declaration.name}`,
|
|
89
|
+
...formatInputs(declaration.inputs),
|
|
90
|
+
` title ${formatExpression(declaration.title)}`,
|
|
91
|
+
...(declaration.description ? [` description ${formatExpression(declaration.description)}`] : []),
|
|
92
|
+
` main render ${formatExpression(declaration.main)}`,
|
|
93
|
+
];
|
|
86
94
|
case "route":
|
|
87
95
|
return [
|
|
88
96
|
`route ${declaration.name}`,
|
|
@@ -176,6 +184,8 @@ function formatPolicyStatement(statement: PointSemanticPolicyStatement): string
|
|
|
176
184
|
|
|
177
185
|
function formatViewStatement(statement: PointSemanticViewStatement): string {
|
|
178
186
|
if (statement.kind === "whenRender") return `when ${formatExpression(statement.condition)} render ${formatExpression(statement.value)}`;
|
|
187
|
+
if (statement.kind === "bindCheckbox") return `bind checkbox "${statement.label}" to ${formatExpression(statement.target)}`;
|
|
188
|
+
if (statement.kind === "onChangeCall") return `on change call ${statement.callback}`;
|
|
179
189
|
return `render ${formatExpression(statement.value)}`;
|
|
180
190
|
}
|
|
181
191
|
|
package/src/semantic/metadata.ts
CHANGED
|
@@ -30,6 +30,9 @@ export function semanticDeclarationMetadata(declaration: PointSemanticDeclaratio
|
|
|
30
30
|
effects: [],
|
|
31
31
|
};
|
|
32
32
|
}
|
|
33
|
+
if (declaration.kind === "page") {
|
|
34
|
+
return { kind: "page", name: declaration.name, outputName: "page", effects: [] };
|
|
35
|
+
}
|
|
33
36
|
if (declaration.kind === "route") {
|
|
34
37
|
return { kind: "route", name: declaration.name, outputName: declaration.output.name, effects: [] };
|
|
35
38
|
}
|
package/src/semantic/naming.ts
CHANGED
|
@@ -11,7 +11,7 @@ export function toIdentifier(label: string): string {
|
|
|
11
11
|
export function semanticFunctionName(
|
|
12
12
|
label: string,
|
|
13
13
|
outputName: string,
|
|
14
|
-
kind: "calculation" | "rule" | "label" | "action" | "policy" | "view" | "route" | "workflow" | "command",
|
|
14
|
+
kind: "calculation" | "rule" | "label" | "action" | "policy" | "view" | "page" | "route" | "workflow" | "command",
|
|
15
15
|
): string {
|
|
16
16
|
const base = toIdentifier(label);
|
|
17
17
|
const suffix =
|
|
@@ -21,7 +21,9 @@ export function semanticFunctionName(
|
|
|
21
21
|
? "Policy"
|
|
22
22
|
: kind === "view"
|
|
23
23
|
? "View"
|
|
24
|
-
: kind === "
|
|
24
|
+
: kind === "page"
|
|
25
|
+
? "Page"
|
|
26
|
+
: kind === "route"
|
|
25
27
|
? "Route"
|
|
26
28
|
: kind === "workflow"
|
|
27
29
|
? "Workflow"
|
package/src/semantic/parse.ts
CHANGED
|
@@ -25,6 +25,7 @@ import type {
|
|
|
25
25
|
PointSemanticUseDeclaration,
|
|
26
26
|
PointSemanticViewDeclaration,
|
|
27
27
|
PointSemanticViewStatement,
|
|
28
|
+
PointSemanticPageDeclaration,
|
|
28
29
|
PointSemanticWorkflowDeclaration,
|
|
29
30
|
PointSemanticWorkflowStatement,
|
|
30
31
|
PointSemanticBinding,
|
|
@@ -129,6 +130,12 @@ export function parseSemanticSource(source: string): PointSemanticProgram {
|
|
|
129
130
|
index = parsed.next;
|
|
130
131
|
continue;
|
|
131
132
|
}
|
|
133
|
+
if (trimmed.startsWith("page ")) {
|
|
134
|
+
const parsed = parsePage(lines, index, source, records, callables);
|
|
135
|
+
declarations.push(parsed.declaration);
|
|
136
|
+
index = parsed.next;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
132
139
|
if (trimmed.startsWith("route ")) {
|
|
133
140
|
const parsed = parseRoute(lines, index, source, records, callables);
|
|
134
141
|
declarations.push(parsed.declaration);
|
|
@@ -586,6 +593,24 @@ function parseView(
|
|
|
586
593
|
});
|
|
587
594
|
continue;
|
|
588
595
|
}
|
|
596
|
+
const bindCheckbox = line.match(/^bind checkbox "(.+)" to (.+)$/);
|
|
597
|
+
if (bindCheckbox) {
|
|
598
|
+
statements.push({
|
|
599
|
+
kind: "bindCheckbox",
|
|
600
|
+
label: bindCheckbox[1] ?? "",
|
|
601
|
+
target: parseLineExpression(bindCheckbox[2] ?? "", context, source, lineNumber),
|
|
602
|
+
span: lineSpan(source, lineNumber),
|
|
603
|
+
});
|
|
604
|
+
continue;
|
|
605
|
+
}
|
|
606
|
+
if (line.startsWith("on change call ")) {
|
|
607
|
+
statements.push({
|
|
608
|
+
kind: "onChangeCall",
|
|
609
|
+
callback: line.slice("on change call ".length).trim(),
|
|
610
|
+
span: lineSpan(source, lineNumber),
|
|
611
|
+
});
|
|
612
|
+
continue;
|
|
613
|
+
}
|
|
589
614
|
throw new Error(`Unknown view statement: ${line}`);
|
|
590
615
|
}
|
|
591
616
|
|
|
@@ -595,6 +620,57 @@ function parseView(
|
|
|
595
620
|
};
|
|
596
621
|
}
|
|
597
622
|
|
|
623
|
+
function parsePage(
|
|
624
|
+
lines: string[],
|
|
625
|
+
start: number,
|
|
626
|
+
source: string,
|
|
627
|
+
records: Map<string, Map<string, string>>,
|
|
628
|
+
callables: string[],
|
|
629
|
+
): { declaration: PointSemanticPageDeclaration; next: number } {
|
|
630
|
+
const name = (lines[start] ?? "").trim().slice("page ".length).trim();
|
|
631
|
+
const body = collectSemanticBody(lines, start + 1);
|
|
632
|
+
const inputs: PointSemanticBinding[] = [];
|
|
633
|
+
const paramTypes = new Map<string, string>();
|
|
634
|
+
const bindings: string[] = [];
|
|
635
|
+
let title: PointSemanticPageDeclaration["title"] | undefined;
|
|
636
|
+
let description: PointSemanticPageDeclaration["description"];
|
|
637
|
+
let main: PointSemanticPageDeclaration["main"] | undefined;
|
|
638
|
+
|
|
639
|
+
for (let lineIndex = 0; lineIndex < body.lines.length; lineIndex += 1) {
|
|
640
|
+
const line = body.lines[lineIndex] ?? "";
|
|
641
|
+
const lineNumber = body.lineNumbers[lineIndex] ?? start + 2;
|
|
642
|
+
if (line.startsWith("input ")) {
|
|
643
|
+
const binding = parseInputBinding(line.slice("input ".length), source, lineNumber);
|
|
644
|
+
inputs.push(binding);
|
|
645
|
+
paramTypes.set(binding.label, typeLabel(binding.type));
|
|
646
|
+
bindings.push(binding.label);
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
const context = buildExpressionContext({ bindings, paramTypes, recordFields: records, callables });
|
|
650
|
+
if (line.startsWith("title ")) {
|
|
651
|
+
title = parseLineExpression(line.slice("title ".length), context, source, lineNumber);
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
if (line.startsWith("description ")) {
|
|
655
|
+
description = parseLineExpression(line.slice("description ".length), context, source, lineNumber);
|
|
656
|
+
continue;
|
|
657
|
+
}
|
|
658
|
+
if (line.startsWith("main render ")) {
|
|
659
|
+
main = parseLineExpression(line.slice("main render ".length), context, source, lineNumber);
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
throw new Error(`Unknown page statement: ${line}`);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
if (!title) throw new Error(`Page ${name} requires a title`);
|
|
666
|
+
if (!main) throw new Error(`Page ${name} requires main render`);
|
|
667
|
+
|
|
668
|
+
return {
|
|
669
|
+
declaration: { kind: "page", name, inputs, title, description, main, span: lineSpan(source, start + 1) },
|
|
670
|
+
next: body.next,
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
|
|
598
674
|
function parseRoute(
|
|
599
675
|
lines: string[],
|
|
600
676
|
start: number,
|
|
@@ -893,7 +969,7 @@ function collectSemanticBody(lines: string[], start: number): SemanticBody {
|
|
|
893
969
|
}
|
|
894
970
|
|
|
895
971
|
function isSemanticTopLevel(line: string): boolean {
|
|
896
|
-
return /^(module|use|record|calculation|rule|label|external|action|policy|view|route|workflow|command)\s+/.test(line);
|
|
972
|
+
return /^(module|use|record|calculation|rule|label|external|action|policy|view|page|route|workflow|command)\s+/.test(line);
|
|
897
973
|
}
|
|
898
974
|
|
|
899
975
|
function isLoopBoundary(line: string): boolean {
|
|
@@ -914,7 +990,10 @@ function typeLabel(type: { kind: "typeRef"; name: string; args: unknown[] }): st
|
|
|
914
990
|
if (type.name === "Maybe" && type.args[0]) {
|
|
915
991
|
return `Maybe<${typeLabel(type.args[0] as { kind: "typeRef"; name: string; args: unknown[] })}>`;
|
|
916
992
|
}
|
|
917
|
-
|
|
993
|
+
if (type.name === "Handler" && type.args[0]) {
|
|
994
|
+
return `Handler ${typeLabel(type.args[0] as { kind: "typeRef"; name: string; args: unknown[] })}`;
|
|
995
|
+
}
|
|
996
|
+
const primitives = new Set(["Text", "Int", "Float", "Bool", "Void", "Maybe", "Or", "Error", "Page", "Handler"]);
|
|
918
997
|
if (primitives.has(type.name)) return type.name;
|
|
919
998
|
return toPascalCase(type.name);
|
|
920
999
|
}
|
package/src/std/http.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
type PointStdError = { message: string };
|
|
2
|
+
|
|
3
|
+
async function readResponseText(response: Response): Promise<string | PointStdError> {
|
|
4
|
+
if (!response.ok) {
|
|
5
|
+
return { message: `HTTP ${response.status}: ${response.statusText}` };
|
|
6
|
+
}
|
|
7
|
+
return response.text();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function httpGet(url: string): Promise<string | PointStdError> {
|
|
11
|
+
try {
|
|
12
|
+
return await readResponseText(await fetch(url));
|
|
13
|
+
} catch (error) {
|
|
14
|
+
return { message: error instanceof Error ? error.message : String(error) };
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function httpPost(url: string, body: string): Promise<string | PointStdError> {
|
|
19
|
+
try {
|
|
20
|
+
return await readResponseText(
|
|
21
|
+
await fetch(url, {
|
|
22
|
+
method: "POST",
|
|
23
|
+
headers: { "Content-Type": "text/plain; charset=utf-8" },
|
|
24
|
+
body,
|
|
25
|
+
}),
|
|
26
|
+
);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
return { message: error instanceof Error ? error.message : String(error) };
|
|
29
|
+
}
|
|
30
|
+
}
|
package/src/std/json.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type PointStdError = { message: string };
|
|
2
|
+
|
|
3
|
+
export function jsonParse(value: string): string | PointStdError {
|
|
4
|
+
try {
|
|
5
|
+
return JSON.stringify(JSON.parse(value));
|
|
6
|
+
} catch (error) {
|
|
7
|
+
return { message: error instanceof Error ? error.message : String(error) };
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function jsonStringify(value: string): string {
|
|
12
|
+
return JSON.stringify(JSON.parse(value));
|
|
13
|
+
}
|