@hatchingpoint/point 0.0.12 → 0.0.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +8 -2
- package/src/cli.ts +0 -0
- package/src/core/ast.ts +21 -1
- package/src/core/check.ts +10 -2
- package/src/core/cli.ts +87 -23
- package/src/core/context.ts +3 -0
- package/src/core/emit-javascript.ts +5 -1
- package/src/core/emit-python.ts +22 -7
- package/src/core/emit-typescript.ts +84 -3
- package/src/core/index.ts +1 -0
- package/src/core/packages.ts +230 -0
- package/src/core/run-bridge.ts +93 -0
- package/src/core/semantic-source.ts +2 -2
- package/src/core/test-only/legacy-lowering.ts +76 -7
- package/src/semantic/ast.ts +14 -1
- package/src/semantic/callables.ts +2 -1
- package/src/semantic/context.ts +12 -1
- package/src/semantic/desugar.ts +82 -3
- package/src/semantic/expressions.ts +8 -0
- package/src/semantic/format.ts +10 -0
- package/src/semantic/metadata.ts +3 -0
- package/src/semantic/naming.ts +4 -2
- package/src/semantic/parse.ts +81 -2
- package/src/std/env.ts +4 -0
- package/src/std/fs.ts +19 -0
- package/src/std/http.ts +30 -0
- package/src/std/json.ts +13 -0
- package/src/std/text.ts +15 -0
- package/src/std/time.ts +15 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
3
|
+
import { join, relative, resolve } from "node:path";
|
|
4
|
+
|
|
5
|
+
export const POINT_MANIFEST = "point.json";
|
|
6
|
+
export const POINT_LOCK = "point.lock";
|
|
7
|
+
export const LOCK_SCHEMA = "point.lock.v1";
|
|
8
|
+
|
|
9
|
+
export interface PointManifest {
|
|
10
|
+
name: string;
|
|
11
|
+
version: string;
|
|
12
|
+
dependencies?: Record<string, string>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface PointLock {
|
|
16
|
+
schemaVersion: typeof LOCK_SCHEMA;
|
|
17
|
+
packages: Record<string, PointLockPackage>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface PointLockPackage {
|
|
21
|
+
version: string;
|
|
22
|
+
path?: string;
|
|
23
|
+
dependencies?: Record<string, string>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type DependencySpecKind = "workspace" | "file" | "npm";
|
|
27
|
+
|
|
28
|
+
export interface ParsedDependencySpec {
|
|
29
|
+
kind: DependencySpecKind;
|
|
30
|
+
locator: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ParsedNpmLocator {
|
|
34
|
+
name: string;
|
|
35
|
+
version?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function parseNpmLocator(locator: string): ParsedNpmLocator {
|
|
39
|
+
if (locator.startsWith("@")) {
|
|
40
|
+
const slash = locator.indexOf("/");
|
|
41
|
+
if (slash < 0) {
|
|
42
|
+
throw new Error(`Invalid npm package name "${locator}". Scoped packages use @scope/name.`);
|
|
43
|
+
}
|
|
44
|
+
const rest = locator.slice(slash + 1);
|
|
45
|
+
const versionAt = rest.indexOf("@");
|
|
46
|
+
if (versionAt >= 0) {
|
|
47
|
+
return { name: `${locator.slice(0, slash + 1 + versionAt)}`, version: rest.slice(versionAt + 1) };
|
|
48
|
+
}
|
|
49
|
+
return { name: locator };
|
|
50
|
+
}
|
|
51
|
+
const versionAt = locator.lastIndexOf("@");
|
|
52
|
+
if (versionAt > 0) {
|
|
53
|
+
return { name: locator.slice(0, versionAt), version: locator.slice(versionAt + 1) };
|
|
54
|
+
}
|
|
55
|
+
return { name: locator };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function parseDependencySpec(spec: string): ParsedDependencySpec {
|
|
59
|
+
if (spec.startsWith("workspace:")) return { kind: "workspace", locator: spec.slice("workspace:".length) };
|
|
60
|
+
if (spec.startsWith("file:")) return { kind: "file", locator: spec.slice("file:".length) };
|
|
61
|
+
if (spec.startsWith("npm:")) return { kind: "npm", locator: spec.slice("npm:".length) };
|
|
62
|
+
throw new Error(`Invalid dependency spec "${spec}". Use workspace:<path>, file:<path>, or npm:<package>[@version].`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function readPointManifest(cwd = process.cwd()): Promise<PointManifest> {
|
|
66
|
+
const path = join(cwd, POINT_MANIFEST);
|
|
67
|
+
if (!existsSync(path)) throw new Error(`Missing ${POINT_MANIFEST} in ${cwd}`);
|
|
68
|
+
return Bun.file(path).json() as Promise<PointManifest>;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function writePointManifest(manifest: PointManifest, cwd = process.cwd()): Promise<void> {
|
|
72
|
+
const path = join(cwd, POINT_MANIFEST);
|
|
73
|
+
await Bun.write(path, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function readPointLock(cwd = process.cwd()): Promise<PointLock | null> {
|
|
77
|
+
const path = join(cwd, POINT_LOCK);
|
|
78
|
+
if (!existsSync(path)) return null;
|
|
79
|
+
const lock = (await Bun.file(path).json()) as PointLock;
|
|
80
|
+
if (lock.schemaVersion !== LOCK_SCHEMA) {
|
|
81
|
+
throw new Error(`Unsupported ${POINT_LOCK} schema: ${lock.schemaVersion}`);
|
|
82
|
+
}
|
|
83
|
+
return lock;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function writePointLock(lock: PointLock, cwd = process.cwd()): Promise<void> {
|
|
87
|
+
const path = join(cwd, POINT_LOCK);
|
|
88
|
+
await Bun.write(path, `${JSON.stringify(lock, null, 2)}\n`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function normalizePackagePath(cwd: string, rawPath: string): string {
|
|
92
|
+
const absolute = resolve(cwd, rawPath);
|
|
93
|
+
if (!existsSync(absolute)) {
|
|
94
|
+
throw new Error(`Dependency path does not exist: ${rawPath}`);
|
|
95
|
+
}
|
|
96
|
+
return relative(cwd, absolute).split("\\").join("/") || ".";
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function locatePointPackageRoot(pkgDir: string): string {
|
|
100
|
+
if (existsSync(join(pkgDir, POINT_MANIFEST))) return pkgDir;
|
|
101
|
+
const srcDir = join(pkgDir, "src");
|
|
102
|
+
if (existsSync(srcDir) && readdirSync(srcDir).some((name) => name.endsWith(".point"))) return pkgDir;
|
|
103
|
+
if (readdirSync(pkgDir).some((name) => name.endsWith(".point"))) return pkgDir;
|
|
104
|
+
throw new Error(`Point package at ${pkgDir} has no ${POINT_MANIFEST} or src/*.point modules`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function resolveNpmPackagePath(cwd: string, packageName: string): string | null {
|
|
108
|
+
const direct = join(cwd, "node_modules", ...packageName.split("/"));
|
|
109
|
+
if (existsSync(join(direct, "package.json"))) return direct;
|
|
110
|
+
try {
|
|
111
|
+
const req = createRequire(join(cwd, "package.json"));
|
|
112
|
+
const pkgJson = req.resolve(`${packageName}/package.json`);
|
|
113
|
+
return resolve(pkgJson, "..");
|
|
114
|
+
} catch {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function readInstalledNpmVersion(pkgDir: string): Promise<string | null> {
|
|
120
|
+
const pkgJsonPath = join(pkgDir, "package.json");
|
|
121
|
+
if (!existsSync(pkgJsonPath)) return null;
|
|
122
|
+
const pkg = (await Bun.file(pkgJsonPath).json()) as { version?: string };
|
|
123
|
+
return pkg.version ?? null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export async function ensureNpmPackage(cwd: string, packageName: string, version?: string): Promise<string> {
|
|
127
|
+
const installed = resolveNpmPackagePath(cwd, packageName);
|
|
128
|
+
if (installed) {
|
|
129
|
+
const installedVersion = await readInstalledNpmVersion(installed);
|
|
130
|
+
if (!version || installedVersion === version) return installed;
|
|
131
|
+
}
|
|
132
|
+
const installSpec = version ? `${packageName}@${version}` : packageName;
|
|
133
|
+
const result = await Bun.$`npm install ${installSpec} --prefix ${cwd} --no-save --no-package-lock`.quiet().nothrow();
|
|
134
|
+
if (result.exitCode !== 0) {
|
|
135
|
+
const detail = result.stderr.toString().trim() || result.stdout.toString().trim();
|
|
136
|
+
throw new Error(`npm install failed for ${installSpec}: ${detail || "unknown error"}`);
|
|
137
|
+
}
|
|
138
|
+
const resolved = resolveNpmPackagePath(cwd, packageName);
|
|
139
|
+
if (!resolved) {
|
|
140
|
+
throw new Error(`npm package "${packageName}" was not found under node_modules/ after install`);
|
|
141
|
+
}
|
|
142
|
+
if (version) {
|
|
143
|
+
const installedVersion = await readInstalledNpmVersion(resolved);
|
|
144
|
+
if (installedVersion && installedVersion !== version) {
|
|
145
|
+
throw new Error(`npm package "${packageName}" resolved to ${installedVersion}, expected ${version}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return resolved;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export async function resolveNpmDependencySpec(spec: string, cwd = process.cwd()): Promise<PointLockPackage> {
|
|
152
|
+
const parsed = parseDependencySpec(spec);
|
|
153
|
+
if (parsed.kind !== "npm") throw new Error(`Expected npm: spec, got ${spec}`);
|
|
154
|
+
const { name, version } = parseNpmLocator(parsed.locator);
|
|
155
|
+
const pkgDir = await ensureNpmPackage(cwd, name, version);
|
|
156
|
+
locatePointPackageRoot(pkgDir);
|
|
157
|
+
const npmVersion = (await readInstalledNpmVersion(pkgDir)) ?? version ?? "npm";
|
|
158
|
+
return { version: npmVersion, path: normalizePackagePath(cwd, pkgDir) };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export async function resolveDependencySpec(spec: string, cwd = process.cwd()): Promise<PointLockPackage> {
|
|
162
|
+
const parsed = parseDependencySpec(spec);
|
|
163
|
+
if (parsed.kind === "npm") return resolveNpmDependencySpec(spec, cwd);
|
|
164
|
+
const path = normalizePackagePath(cwd, parsed.locator);
|
|
165
|
+
return { version: parsed.kind, path };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export async function resolveLockFromManifest(manifest: PointManifest, cwd = process.cwd()): Promise<PointLock> {
|
|
169
|
+
const packages: Record<string, PointLockPackage> = {
|
|
170
|
+
[manifest.name]: {
|
|
171
|
+
version: manifest.version,
|
|
172
|
+
dependencies: manifest.dependencies ?? {},
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
for (const [name, spec] of Object.entries(manifest.dependencies ?? {})) {
|
|
176
|
+
packages[name] = await resolveDependencySpec(spec, cwd);
|
|
177
|
+
const entry = packages[name];
|
|
178
|
+
if (!entry.path) continue;
|
|
179
|
+
const nestedManifestPath = join(cwd, entry.path, POINT_MANIFEST);
|
|
180
|
+
if (!existsSync(nestedManifestPath)) continue;
|
|
181
|
+
const nested = (await Bun.file(nestedManifestPath).json()) as PointManifest;
|
|
182
|
+
packages[nested.name] = {
|
|
183
|
+
version: nested.version,
|
|
184
|
+
dependencies: nested.dependencies ?? {},
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
return { schemaVersion: LOCK_SCHEMA, packages };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export async function addPointDependency(name: string, spec: string, cwd = process.cwd()): Promise<{ manifest: PointManifest; lock: PointLock }> {
|
|
191
|
+
if (!name || !/^[A-Za-z][A-Za-z0-9_-]*$/.test(name)) {
|
|
192
|
+
throw new Error(`Invalid dependency name "${name}". Use an identifier like std or point-logic.`);
|
|
193
|
+
}
|
|
194
|
+
parseDependencySpec(spec);
|
|
195
|
+
const manifest = await readPointManifest(cwd);
|
|
196
|
+
manifest.dependencies = { ...(manifest.dependencies ?? {}), [name]: spec };
|
|
197
|
+
const lock = await resolveLockFromManifest(manifest, cwd);
|
|
198
|
+
await writePointManifest(manifest, cwd);
|
|
199
|
+
await writePointLock(lock, cwd);
|
|
200
|
+
return { manifest, lock };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function packageRootFromLock(lock: PointLock | null, packageName: string): string | null {
|
|
204
|
+
const entry = lock?.packages[packageName];
|
|
205
|
+
if (entry?.path) return entry.path;
|
|
206
|
+
if (!lock && packageName === "std") return "std";
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function modulePathCandidates(root: string, modulePath: string): string[] {
|
|
211
|
+
const segments = modulePath.replaceAll(".", "/");
|
|
212
|
+
return [`${root}/${segments}.point`, `${root}/src/${segments}.point`];
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function modulePathFromLock(lock: PointLock | null, moduleName: string, cwd = process.cwd()): string {
|
|
216
|
+
const dot = moduleName.indexOf(".");
|
|
217
|
+
if (dot < 0) {
|
|
218
|
+
throw new Error(`Use declarations without from must target package modules: ${moduleName}`);
|
|
219
|
+
}
|
|
220
|
+
const packageName = moduleName.slice(0, dot);
|
|
221
|
+
const modulePath = moduleName.slice(dot + 1);
|
|
222
|
+
const root = packageRootFromLock(lock, packageName);
|
|
223
|
+
if (!root) {
|
|
224
|
+
throw new Error(`Unknown package "${packageName}" in ${moduleName}. Add it with: point add ${packageName} <spec>`);
|
|
225
|
+
}
|
|
226
|
+
for (const candidate of modulePathCandidates(root, modulePath)) {
|
|
227
|
+
if (existsSync(join(cwd, candidate))) return candidate;
|
|
228
|
+
}
|
|
229
|
+
return modulePathCandidates(root, modulePath)[0]!;
|
|
230
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { PointCoreProgram } from "./ast.ts";
|
|
2
|
+
import { emitPointCoreJavaScript } from "./emit-javascript.ts";
|
|
3
|
+
import { isPureLogicProgram } from "./emit-python.ts";
|
|
4
|
+
|
|
5
|
+
/** Pure logic with no imports or externals can run in-memory without a temp module file. */
|
|
6
|
+
export function canBundleRunInMemory(program: PointCoreProgram): boolean {
|
|
7
|
+
if (!isPureLogicProgram(program)) return false;
|
|
8
|
+
if (program.declarations.some((declaration) => declaration.kind === "import" || declaration.kind === "external")) return false;
|
|
9
|
+
if (program.semanticSource?.uses?.length) return false;
|
|
10
|
+
const emitted = emitPointCoreJavaScript(program);
|
|
11
|
+
if (/^\s*import\s/m.test(emitted)) return false;
|
|
12
|
+
return !hasUnresolvedCallTargets(emitted);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const BUILTIN_CALL_TARGETS = new Set([
|
|
16
|
+
"Array",
|
|
17
|
+
"Boolean",
|
|
18
|
+
"JSON",
|
|
19
|
+
"Math",
|
|
20
|
+
"Number",
|
|
21
|
+
"Object",
|
|
22
|
+
"Promise",
|
|
23
|
+
"String",
|
|
24
|
+
"console",
|
|
25
|
+
"fetch",
|
|
26
|
+
"parseInt",
|
|
27
|
+
"parseFloat",
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
const NON_CALL_KEYWORDS = new Set([
|
|
31
|
+
"async",
|
|
32
|
+
"await",
|
|
33
|
+
"catch",
|
|
34
|
+
"delete",
|
|
35
|
+
"export",
|
|
36
|
+
"for",
|
|
37
|
+
"function",
|
|
38
|
+
"if",
|
|
39
|
+
"import",
|
|
40
|
+
"instanceof",
|
|
41
|
+
"new",
|
|
42
|
+
"return",
|
|
43
|
+
"switch",
|
|
44
|
+
"throw",
|
|
45
|
+
"typeof",
|
|
46
|
+
"void",
|
|
47
|
+
"while",
|
|
48
|
+
"with",
|
|
49
|
+
"yield",
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
function hasUnresolvedCallTargets(source: string): boolean {
|
|
53
|
+
const defined = new Set<string>();
|
|
54
|
+
for (const match of source.matchAll(/(?:export\s+)?(?:async\s+)?function\s+([A-Za-z_$][\w$]*)/g)) defined.add(match[1]!);
|
|
55
|
+
for (const match of source.matchAll(/\b([A-Za-z_$][\w$]*)\s*\(/g)) {
|
|
56
|
+
const name = match[1]!;
|
|
57
|
+
if (NON_CALL_KEYWORDS.has(name) || BUILTIN_CALL_TARGETS.has(name) || defined.has(name)) continue;
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function bundleJavaScriptForEval(source: string): { body: string; exports: string[] } {
|
|
64
|
+
const exports: string[] = [];
|
|
65
|
+
const bodyLines: string[] = [];
|
|
66
|
+
for (const line of source.split(/\r?\n/)) {
|
|
67
|
+
if (/^\s*import\s/.test(line)) continue;
|
|
68
|
+
const fnMatch = line.match(/^export\s+(async\s+)?function\s+([A-Za-z_$][\w$]*)/);
|
|
69
|
+
if (fnMatch) {
|
|
70
|
+
exports.push(fnMatch[2]!);
|
|
71
|
+
bodyLines.push(line.replace(/^export\s+/, ""));
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const bindingMatch = line.match(/^export\s+(const|let|var)\s+([A-Za-z_$][\w$]*)/);
|
|
75
|
+
if (bindingMatch) {
|
|
76
|
+
exports.push(bindingMatch[2]!);
|
|
77
|
+
bodyLines.push(line.replace(/^export\s+/, ""));
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
bodyLines.push(line);
|
|
81
|
+
}
|
|
82
|
+
return { body: bodyLines.join("\n"), exports };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function executeBundledEntry(program: PointCoreProgram, entryName: string): Promise<unknown> {
|
|
86
|
+
const { body, exports } = bundleJavaScriptForEval(emitPointCoreJavaScript(program));
|
|
87
|
+
if (!exports.includes(entryName)) throw new Error(`Entrypoint ${entryName} was not emitted.`);
|
|
88
|
+
const factory = new Function(`"use strict";\n${body}\nreturn { ${exports.join(", ")} };`);
|
|
89
|
+
const mod = factory() as Record<string, unknown>;
|
|
90
|
+
const entry = mod[entryName];
|
|
91
|
+
if (typeof entry !== "function") throw new Error(`Entrypoint ${entryName} was not a function.`);
|
|
92
|
+
return await (entry as () => unknown)();
|
|
93
|
+
}
|
|
@@ -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}.`;
|