@hatchingpoint/point 0.0.11 → 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/README.md +2 -2
- package/package.json +4 -2
- package/src/cli.ts +0 -0
- package/src/core/ast.ts +21 -1
- package/src/core/check-docs.ts +155 -0
- package/src/core/check.ts +10 -2
- package/src/core/cli.ts +90 -31
- package/src/core/context.ts +3 -0
- package/src/core/emit-javascript.ts +67 -1
- package/src/core/emit-python.ts +188 -0
- package/src/core/emit-typescript.ts +89 -5
- package/src/core/index.ts +3 -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 +28 -3
- 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
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
PointCoreDeclaration,
|
|
3
|
+
PointCoreExpression,
|
|
4
|
+
PointCoreExternalDeclaration,
|
|
5
|
+
PointCoreFunctionDeclaration,
|
|
6
|
+
PointCoreParameter,
|
|
7
|
+
PointCorePrimitiveType,
|
|
8
|
+
PointCoreProgram,
|
|
9
|
+
PointCoreStatement,
|
|
10
|
+
PointCoreTypeDeclaration,
|
|
11
|
+
PointCoreTypeExpression,
|
|
12
|
+
PointCoreValueDeclaration,
|
|
13
|
+
} from "./ast.ts";
|
|
14
|
+
|
|
15
|
+
const BINARY_OPERATORS: Record<string, string> = {
|
|
16
|
+
and: "and",
|
|
17
|
+
or: "or",
|
|
18
|
+
};
|
|
19
|
+
|
|
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
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Emit Python from a core AST program (pure logic modules). Production path: parsePointSource → check → emit. */
|
|
28
|
+
export function emitPointCorePython(program: PointCoreProgram): string {
|
|
29
|
+
const lines: string[] = [];
|
|
30
|
+
lines.push("# Generated by Point. Do not edit directly.");
|
|
31
|
+
if (program.module) lines.push(`# Point module: ${program.module}`);
|
|
32
|
+
lines.push("");
|
|
33
|
+
lines.push("from __future__ import annotations");
|
|
34
|
+
lines.push("");
|
|
35
|
+
if (program.declarations.some((declaration) => declaration.kind === "type")) {
|
|
36
|
+
lines.push("from typing import TypedDict");
|
|
37
|
+
lines.push("");
|
|
38
|
+
}
|
|
39
|
+
for (const declaration of program.declarations) {
|
|
40
|
+
const emitted = emitDeclaration(declaration);
|
|
41
|
+
if (emitted.length > 0) lines.push(...emitted, "");
|
|
42
|
+
}
|
|
43
|
+
return `${trimTrailingBlankLines(lines).join("\n")}\n`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function emitDeclaration(declaration: PointCoreDeclaration): string[] {
|
|
47
|
+
if (declaration.kind === "import") {
|
|
48
|
+
const moduleName = declaration.from.replace(/^\.\//, "").replace(/-/g, "_");
|
|
49
|
+
return [`from ${toPythonModuleName(moduleName)} import ${declaration.names.join(", ")}`];
|
|
50
|
+
}
|
|
51
|
+
if (declaration.kind === "external") return emitExternal(declaration);
|
|
52
|
+
if (declaration.kind === "type") return emitType(declaration);
|
|
53
|
+
if (declaration.kind === "value") return [emitValue(declaration)];
|
|
54
|
+
if (declaration.semantic && UNSUPPORTED_SEMANTIC_KINDS.has(declaration.semantic.kind)) {
|
|
55
|
+
return [`# Point: ${declaration.semantic.kind} blocks are not supported in Python emit yet`];
|
|
56
|
+
}
|
|
57
|
+
return emitFunction(declaration);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function emitType(declaration: PointCoreTypeDeclaration): string[] {
|
|
61
|
+
return [
|
|
62
|
+
`class ${declaration.name}(TypedDict):`,
|
|
63
|
+
...declaration.fields.map((field) => ` ${field.name}: ${emitTypeExpression(field.type)}`),
|
|
64
|
+
];
|
|
65
|
+
}
|
|
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
|
+
|
|
80
|
+
function emitFunction(declaration: PointCoreFunctionDeclaration): string[] {
|
|
81
|
+
const asyncPrefix = declaration.semantic?.kind === "action" || declaration.semantic?.kind === "workflow" || declaration.semantic?.kind === "command" ? "async " : "";
|
|
82
|
+
return [
|
|
83
|
+
`${asyncPrefix}def ${declaration.name}(${declaration.params.map(emitParam).join(", ")}) -> ${emitReturnType(declaration)}:`,
|
|
84
|
+
...indentLines(declaration.body.flatMap((statement) => emitStatement(statement, declaration.semantic?.kind))),
|
|
85
|
+
];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function emitReturnType(declaration: PointCoreFunctionDeclaration): string {
|
|
89
|
+
if (declaration.semantic?.kind === "action" || declaration.semantic?.kind === "workflow" || declaration.semantic?.kind === "command") {
|
|
90
|
+
return emitTypeExpression(declaration.returnType);
|
|
91
|
+
}
|
|
92
|
+
return emitTypeExpression(declaration.returnType);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function emitStatement(statement: PointCoreStatement, semanticKind?: string): string[] {
|
|
96
|
+
if (statement.kind === "return") {
|
|
97
|
+
if (semanticKind === "view" || semanticKind === "page") return ["# Point: view/page return values are not supported in Python emit yet", "return \"\""];
|
|
98
|
+
return [statement.value ? `return ${emitExpression(statement.value)}` : "return"];
|
|
99
|
+
}
|
|
100
|
+
if (statement.kind === "value") return [emitValue(statement)];
|
|
101
|
+
if (statement.kind === "assignment") return [`${statement.name} ${statement.operator} ${emitExpression(statement.value)}`];
|
|
102
|
+
if (statement.kind === "if") {
|
|
103
|
+
const lines = [`if ${emitCondition(statement.condition)}:`, ...indentLines(statement.thenBody.flatMap((child) => emitStatement(child, semanticKind)))];
|
|
104
|
+
if (statement.elseBody.length > 0) {
|
|
105
|
+
lines.push("else:", ...indentLines(statement.elseBody.flatMap((child) => emitStatement(child, semanticKind))));
|
|
106
|
+
}
|
|
107
|
+
return lines;
|
|
108
|
+
}
|
|
109
|
+
if (statement.kind === "for") {
|
|
110
|
+
return [`for ${statement.itemName} in ${emitExpression(statement.iterable)}:`, ...indentLines(statement.body.flatMap((child) => emitStatement(child, semanticKind)))];
|
|
111
|
+
}
|
|
112
|
+
return [emitExpression(statement.value)];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function emitValue(declaration: PointCoreValueDeclaration): string {
|
|
116
|
+
return `${declaration.name}: ${emitTypeExpression(declaration.type)} = ${emitExpression(declaration.value)}`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function emitParam(param: PointCoreParameter): string {
|
|
120
|
+
return `${param.name}: ${emitTypeExpression(param.type)}`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function emitTypeExpression(type: PointCoreTypeExpression): string {
|
|
124
|
+
if (type.name === "List") return `list[${type.args[0] ? emitTypeExpression(type.args[0]) : "object"}]`;
|
|
125
|
+
if (type.name === "Maybe") return `${type.args[0] ? emitTypeExpression(type.args[0]) : "object"} | None`;
|
|
126
|
+
if (type.name === "Or") return type.args.map(emitTypeExpression).join(" | ");
|
|
127
|
+
if (type.name === "Error") return "dict[str, str]";
|
|
128
|
+
if (isPrimitiveType(type.name)) return emitPrimitiveType(type.name);
|
|
129
|
+
return type.name;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function emitPrimitiveType(type: PointCorePrimitiveType): string {
|
|
133
|
+
if (type === "Text") return "str";
|
|
134
|
+
if (type === "Int") return "int";
|
|
135
|
+
if (type === "Float") return "float";
|
|
136
|
+
if (type === "Bool") return "bool";
|
|
137
|
+
return "None";
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function emitExpression(expression: PointCoreExpression): string {
|
|
141
|
+
if (expression.kind === "literal") return emitLiteral(expression.value);
|
|
142
|
+
if (expression.kind === "identifier") return expression.name;
|
|
143
|
+
if (expression.kind === "list") return `[${expression.items.map(emitExpression).join(", ")}]`;
|
|
144
|
+
if (expression.kind === "record") {
|
|
145
|
+
return `{${expression.fields.map((field) => `"${field.name}": ${emitExpression(field.value)}`).join(", ")}}`;
|
|
146
|
+
}
|
|
147
|
+
if (expression.kind === "await") return `await ${emitExpression(expression.value)}`;
|
|
148
|
+
if (expression.kind === "property") return `${emitExpression(expression.target)}[${JSON.stringify(expression.name)}]`;
|
|
149
|
+
if (expression.kind === "call") {
|
|
150
|
+
if (expression.callee === "Error") {
|
|
151
|
+
const message = expression.args[0] ? emitExpression(expression.args[0]) : '""';
|
|
152
|
+
return `{"message": ${message}}`;
|
|
153
|
+
}
|
|
154
|
+
return `${expression.callee}(${expression.args.map(emitExpression).join(", ")})`;
|
|
155
|
+
}
|
|
156
|
+
const operator = BINARY_OPERATORS[expression.operator] ?? expression.operator;
|
|
157
|
+
return `(${emitExpression(expression.left)} ${operator} ${emitExpression(expression.right)})`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function emitLiteral(value: unknown): string {
|
|
161
|
+
if (value === null) return "None";
|
|
162
|
+
if (value === true) return "True";
|
|
163
|
+
if (value === false) return "False";
|
|
164
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
165
|
+
return String(value);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function emitCondition(expression: PointCoreExpression): string {
|
|
169
|
+
const emitted = emitExpression(expression);
|
|
170
|
+
return emitted.startsWith("(") && emitted.endsWith(")") ? emitted.slice(1, -1) : emitted;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function toPythonModuleName(from: string): string {
|
|
174
|
+
return from.replace(/\.ts$/, "").replace(/\.js$/, "").replace(/\.py$/, "");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function isPrimitiveType(type: string): type is PointCorePrimitiveType {
|
|
178
|
+
return type === "Text" || type === "Int" || type === "Float" || type === "Bool" || type === "Void";
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function indentLines(lines: string[]): string[] {
|
|
182
|
+
return lines.map((line) => ` ${line}`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function trimTrailingBlankLines(lines: string[]): string[] {
|
|
186
|
+
while (lines.at(-1) === "") lines.pop();
|
|
187
|
+
return lines;
|
|
188
|
+
}
|
|
@@ -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,22 +57,93 @@ 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
|
-
if (semanticKind === "view" && statement.value
|
|
72
|
-
|
|
142
|
+
if (semanticKind === "view" && statement.value) {
|
|
143
|
+
if (statement.value.kind === "literal" && typeof statement.value.value === "string") {
|
|
144
|
+
return [`return <>${escapeJsxText(statement.value.value)}</>;`];
|
|
145
|
+
}
|
|
146
|
+
return [`return <>{${emitExpression(statement.value)}}</>;`];
|
|
73
147
|
}
|
|
74
148
|
return [statement.value ? `return ${emitExpression(statement.value)};` : "return;"];
|
|
75
149
|
}
|
|
@@ -111,10 +185,20 @@ function emitValue(declaration: PointCoreValueDeclaration, exported: boolean): s
|
|
|
111
185
|
}
|
|
112
186
|
|
|
113
187
|
function emitParam(param: PointCoreParameter): string {
|
|
114
|
-
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);
|
|
115
196
|
}
|
|
116
197
|
|
|
117
198
|
function emitTypeExpression(type: PointCoreTypeExpression): string {
|
|
199
|
+
if (type.name === "Handler" && type.args.length === 1) {
|
|
200
|
+
return `(value: ${emitTypeExpression(type.args[0]!)}) => void`;
|
|
201
|
+
}
|
|
118
202
|
if (type.name === "List") return `Array<${type.args[0] ? emitTypeExpression(type.args[0]) : "unknown"}>`;
|
|
119
203
|
if (type.name === "Maybe") return `${type.args[0] ? emitTypeExpression(type.args[0]) : "unknown"} | null`;
|
|
120
204
|
if (type.name === "Or") return type.args.map(emitTypeExpression).join(" | ");
|
package/src/core/index.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
export * from "./ast.ts";
|
|
2
2
|
export * from "./check.ts";
|
|
3
3
|
export { findRunEntryName } from "./cli.ts";
|
|
4
|
+
export * from "./check-docs.ts";
|
|
4
5
|
export * from "./context.ts";
|
|
5
6
|
export * from "./emit-typescript.ts";
|
|
6
7
|
export * from "./emit-javascript.ts";
|
|
8
|
+
export * from "./emit-python.ts";
|
|
7
9
|
export * from "./incremental.ts";
|
|
10
|
+
export * from "./packages.ts";
|
|
8
11
|
export * from "./serialize.ts";
|
|
9
12
|
export * from "../semantic/index.ts";
|
|
10
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"
|