@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
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ point check examples/math.point
|
|
|
13
13
|
|
|
14
14
|
Pair with the [Point Language](https://marketplace.visualstudio.com/items?itemName=hatchingpoint.point) extension in VS Code or Cursor.
|
|
15
15
|
|
|
16
|
-
Point's public source language is semantic product logic. The compiler lowers that source into an internal typed core and emits TypeScript for existing JavaScript infrastructure.
|
|
16
|
+
Point's public source language is semantic product logic. The compiler lowers that source into an internal typed core and emits JavaScript by default for Bun and Node. Use `point build-ts` when you need TypeScript for existing typed JavaScript infrastructure.
|
|
17
17
|
|
|
18
18
|
This package is the source of truth for Point. It exposes:
|
|
19
19
|
|
|
@@ -29,6 +29,6 @@ bun run check
|
|
|
29
29
|
bun run build
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
`bun run build` emits
|
|
32
|
+
`bun run build` emits JavaScript into `generated/` by default. Use `bun run build:ts` for TypeScript and `bun run build:ast` when debugging compiler output.
|
|
33
33
|
|
|
34
34
|
When Point is extracted, this package can move into a standalone repo with the same package name and public entrypoints.
|
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 {
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import type { PointCoreDiagnostic } from "./check.ts";
|
|
4
|
+
import { checkPointCore } from "./check.ts";
|
|
5
|
+
import { parsePointSource } from "./parser.ts";
|
|
6
|
+
|
|
7
|
+
const DEFAULT_DOCS_DIR = "docs/site";
|
|
8
|
+
const POINT_FENCE = /```point\r?\n([\s\S]*?)```/g;
|
|
9
|
+
const POINT_FILE_REF = /\b(?:[\w.-]+\/)*[\w.-]+\.point\b/g;
|
|
10
|
+
|
|
11
|
+
export interface DocsCheckItemResult {
|
|
12
|
+
kind: "snippet" | "file";
|
|
13
|
+
source: string;
|
|
14
|
+
label: string;
|
|
15
|
+
line?: number;
|
|
16
|
+
ok: boolean;
|
|
17
|
+
diagnostics: PointCoreDiagnostic[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface DocsCheckResult {
|
|
21
|
+
ok: boolean;
|
|
22
|
+
checked: number;
|
|
23
|
+
items: DocsCheckItemResult[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function discoverDocsSiteMarkdown(docsDir = DEFAULT_DOCS_DIR, cwd = process.cwd()): Promise<string[]> {
|
|
27
|
+
const root = resolve(cwd, docsDir);
|
|
28
|
+
const glob = new Bun.Glob("**/*.md");
|
|
29
|
+
const files: string[] = [];
|
|
30
|
+
for await (const file of glob.scan({ cwd: root, onlyFiles: true })) {
|
|
31
|
+
files.push(`${docsDir}/${file.replaceAll("\\", "/")}`);
|
|
32
|
+
}
|
|
33
|
+
return files.sort((a, b) => a.localeCompare(b));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function extractPointSnippets(markdown: string, source: string): Array<{ label: string; code: string; line: number }> {
|
|
37
|
+
const snippets: Array<{ label: string; code: string; line: number }> = [];
|
|
38
|
+
let index = 0;
|
|
39
|
+
for (const match of markdown.matchAll(POINT_FENCE)) {
|
|
40
|
+
index += 1;
|
|
41
|
+
const code = match[1]?.replace(/\s+$/, "") ?? "";
|
|
42
|
+
const line = markdown.slice(0, match.index ?? 0).split(/\r?\n/).length;
|
|
43
|
+
snippets.push({ label: `${source} snippet ${index}`, code, line });
|
|
44
|
+
}
|
|
45
|
+
return snippets;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function extractPointFileReferences(markdown: string, markdownPath: string, cwd = process.cwd()): string[] {
|
|
49
|
+
const references = new Set<string>();
|
|
50
|
+
for (const match of markdown.matchAll(POINT_FILE_REF)) {
|
|
51
|
+
const candidate = match[0]!;
|
|
52
|
+
if (candidate.endsWith(".point")) references.add(candidate.replaceAll("\\", "/"));
|
|
53
|
+
}
|
|
54
|
+
const resolved: string[] = [];
|
|
55
|
+
for (const reference of references) {
|
|
56
|
+
const absolute = resolvePointFileReference(reference, markdownPath, cwd);
|
|
57
|
+
if (absolute) resolved.push(absolute);
|
|
58
|
+
}
|
|
59
|
+
return resolved.sort((a, b) => a.localeCompare(b));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function resolvePointFileReference(reference: string, markdownPath: string, cwd: string): string | null {
|
|
63
|
+
const candidates = [
|
|
64
|
+
resolve(cwd, reference),
|
|
65
|
+
resolve(cwd, dirname(markdownPath), reference),
|
|
66
|
+
];
|
|
67
|
+
for (const candidate of candidates) {
|
|
68
|
+
if (!existsSync(candidate)) continue;
|
|
69
|
+
return candidate.replace(resolve(cwd), "").replace(/^[/\\]/, "").replaceAll("\\", "/");
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function checkPointSource(source: string, label: string, kind: "snippet" | "file", markdownSource: string, line?: number): DocsCheckItemResult {
|
|
75
|
+
try {
|
|
76
|
+
const program = parsePointSource(source);
|
|
77
|
+
const diagnostics = checkPointCore(program);
|
|
78
|
+
return { kind, source: markdownSource, label, line, ok: diagnostics.length === 0, diagnostics };
|
|
79
|
+
} catch (error) {
|
|
80
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
81
|
+
return {
|
|
82
|
+
kind,
|
|
83
|
+
source: markdownSource,
|
|
84
|
+
label,
|
|
85
|
+
line,
|
|
86
|
+
ok: false,
|
|
87
|
+
diagnostics: [
|
|
88
|
+
{
|
|
89
|
+
code: "parse-error",
|
|
90
|
+
message,
|
|
91
|
+
path: label,
|
|
92
|
+
ref: `point://docs/${label}`,
|
|
93
|
+
severity: "error",
|
|
94
|
+
span: null,
|
|
95
|
+
repair: "Fix the Point syntax in this docs snippet or referenced file.",
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function checkDocs(options: { docsDir?: string; cwd?: string } = {}): Promise<DocsCheckResult> {
|
|
103
|
+
const docsDir = options.docsDir ?? DEFAULT_DOCS_DIR;
|
|
104
|
+
const cwd = options.cwd ?? process.cwd();
|
|
105
|
+
const markdownFiles = await discoverDocsSiteMarkdown(docsDir, cwd);
|
|
106
|
+
const items: DocsCheckItemResult[] = [];
|
|
107
|
+
const checkedFiles = new Set<string>();
|
|
108
|
+
|
|
109
|
+
for (const markdownPath of markdownFiles) {
|
|
110
|
+
const absoluteMarkdownPath = resolve(cwd, markdownPath);
|
|
111
|
+
const markdown = await Bun.file(absoluteMarkdownPath).text();
|
|
112
|
+
|
|
113
|
+
for (const snippet of extractPointSnippets(markdown, markdownPath)) {
|
|
114
|
+
items.push(checkPointSource(snippet.code, snippet.label, "snippet", markdownPath, snippet.line));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
for (const filePath of extractPointFileReferences(markdown, markdownPath, cwd)) {
|
|
118
|
+
if (checkedFiles.has(filePath)) continue;
|
|
119
|
+
checkedFiles.add(filePath);
|
|
120
|
+
const source = await Bun.file(resolve(cwd, filePath)).text();
|
|
121
|
+
items.push(checkPointSource(source, filePath, "file", markdownPath));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return { ok: items.every((item) => item.ok), checked: items.length, items };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export async function runCheckDocs(options: { docsDir?: string; cwd?: string } = {}): Promise<DocsCheckResult> {
|
|
129
|
+
const result = await checkDocs(options);
|
|
130
|
+
if (result.ok) {
|
|
131
|
+
const snippets = result.items.filter((item) => item.kind === "snippet").length;
|
|
132
|
+
const files = result.items.filter((item) => item.kind === "file").length;
|
|
133
|
+
console.log(`Point docs check passed: ${snippets} snippet(s), ${files} file reference(s)`);
|
|
134
|
+
return result;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const failures = result.items.filter((item) => !item.ok);
|
|
138
|
+
console.error(
|
|
139
|
+
JSON.stringify(
|
|
140
|
+
{
|
|
141
|
+
ok: false,
|
|
142
|
+
failures: failures.map((item) => ({
|
|
143
|
+
kind: item.kind,
|
|
144
|
+
source: item.source,
|
|
145
|
+
label: item.label,
|
|
146
|
+
line: item.line,
|
|
147
|
+
diagnostics: item.diagnostics,
|
|
148
|
+
})),
|
|
149
|
+
},
|
|
150
|
+
null,
|
|
151
|
+
2,
|
|
152
|
+
),
|
|
153
|
+
);
|
|
154
|
+
process.exit(1);
|
|
155
|
+
}
|
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,14 +6,19 @@ 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, isPureLogicProgram } from "./emit-python.ts";
|
|
9
10
|
import { formatPointSource } from "./format.ts";
|
|
10
11
|
import { isCacheHit, isIncrementalEnabled, readBuildCache, recordCacheEntry, writeBuildCache } from "./incremental.ts";
|
|
11
12
|
import { parsePointSource } from "./parser.ts";
|
|
13
|
+
import { runCheckDocs } from "./check-docs.ts";
|
|
14
|
+
import { addPointDependency, modulePathFromLock, POINT_LOCK, POINT_MANIFEST, readPointLock } from "./packages.ts";
|
|
12
15
|
import { runPointLspServer } from "../lsp/server.ts";
|
|
13
16
|
|
|
14
17
|
const DEFAULT_INPUT = "examples/math.point";
|
|
15
18
|
const DEFAULT_OUTPUT = "generated/math.ast.json";
|
|
19
|
+
const DEFAULT_JS_OUTPUT = "generated/math.js";
|
|
16
20
|
const DEFAULT_TS_OUTPUT = "generated/math.ts";
|
|
21
|
+
const DEFAULT_PY_OUTPUT = "generated/math.py";
|
|
17
22
|
const DEFAULT_PATTERNS = ["examples/**/*.point", "std/**/*.point", "compiler/**/*.point"];
|
|
18
23
|
const GENERATED_DIR = "generated";
|
|
19
24
|
|
|
@@ -34,6 +39,23 @@ export async function main() {
|
|
|
34
39
|
return;
|
|
35
40
|
}
|
|
36
41
|
|
|
42
|
+
if (command === "check-docs") {
|
|
43
|
+
await runCheckDocs();
|
|
44
|
+
return;
|
|
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
|
+
|
|
37
59
|
const inputPath = resolve(process.cwd(), input);
|
|
38
60
|
const source = await Bun.file(inputPath).text();
|
|
39
61
|
const program = parsePointSource(source);
|
|
@@ -101,15 +123,27 @@ export async function main() {
|
|
|
101
123
|
return;
|
|
102
124
|
}
|
|
103
125
|
|
|
104
|
-
if (command === "build") {
|
|
126
|
+
if (command === "build" || command === "build-js") {
|
|
127
|
+
if (diagnostics.length > 0) {
|
|
128
|
+
console.error(JSON.stringify({ ok: false, diagnostics }, null, 2));
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
const outputPath = resolve(process.cwd(), output === DEFAULT_OUTPUT ? DEFAULT_JS_OUTPUT : output);
|
|
132
|
+
await Bun.$`mkdir -p ${dirname(outputPath)}`.quiet();
|
|
133
|
+
await Bun.write(outputPath, emitPointCoreJavaScript(program));
|
|
134
|
+
console.log(`Point core JavaScript build wrote ${outputPath.replaceAll("\\", "/")}`);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (command === "build-ast") {
|
|
105
139
|
if (diagnostics.length > 0) {
|
|
106
140
|
console.error(JSON.stringify({ ok: false, diagnostics }, null, 2));
|
|
107
141
|
process.exit(1);
|
|
108
142
|
}
|
|
109
|
-
const outputPath = resolve(process.cwd(), output);
|
|
143
|
+
const outputPath = resolve(process.cwd(), output === DEFAULT_OUTPUT ? DEFAULT_OUTPUT : output);
|
|
110
144
|
await Bun.$`mkdir -p ${dirname(outputPath)}`.quiet();
|
|
111
145
|
await Bun.write(outputPath, `${JSON.stringify(program, null, 2)}\n`);
|
|
112
|
-
console.log(`Point core build wrote ${
|
|
146
|
+
console.log(`Point core AST build wrote ${outputPath.replaceAll("\\", "/")}`);
|
|
113
147
|
return;
|
|
114
148
|
}
|
|
115
149
|
|
|
@@ -125,15 +159,15 @@ export async function main() {
|
|
|
125
159
|
return;
|
|
126
160
|
}
|
|
127
161
|
|
|
128
|
-
if (command === "build-
|
|
162
|
+
if (command === "build-py") {
|
|
129
163
|
if (diagnostics.length > 0) {
|
|
130
164
|
console.error(JSON.stringify({ ok: false, diagnostics }, null, 2));
|
|
131
165
|
process.exit(1);
|
|
132
166
|
}
|
|
133
|
-
const outputPath = resolve(process.cwd(), output === DEFAULT_OUTPUT ?
|
|
167
|
+
const outputPath = resolve(process.cwd(), output === DEFAULT_OUTPUT ? DEFAULT_PY_OUTPUT : output);
|
|
134
168
|
await Bun.$`mkdir -p ${dirname(outputPath)}`.quiet();
|
|
135
|
-
await Bun.write(outputPath,
|
|
136
|
-
console.log(`Point core
|
|
169
|
+
await Bun.write(outputPath, emitPointCorePython(program));
|
|
170
|
+
console.log(`Point core Python build wrote ${outputPath.replaceAll("\\", "/")}`);
|
|
137
171
|
return;
|
|
138
172
|
}
|
|
139
173
|
|
|
@@ -142,8 +176,8 @@ export async function main() {
|
|
|
142
176
|
console.error(JSON.stringify({ ok: false, diagnostics }, null, 2));
|
|
143
177
|
process.exit(1);
|
|
144
178
|
}
|
|
145
|
-
const runOutput = resolve(tmpdir(), `point-run-${Date.now()}.
|
|
146
|
-
await Bun.write(runOutput,
|
|
179
|
+
const runOutput = resolve(tmpdir(), `point-run-${Date.now()}.js`);
|
|
180
|
+
await Bun.write(runOutput, emitPointCoreJavaScript(program));
|
|
147
181
|
let entryName: string | null = null;
|
|
148
182
|
try {
|
|
149
183
|
const mod = await import(pathToFileUrl(runOutput));
|
|
@@ -177,8 +211,9 @@ export async function main() {
|
|
|
177
211
|
async function runProjectCommand(command: string) {
|
|
178
212
|
const inputs = await discoverInputs();
|
|
179
213
|
if (inputs.length === 0) throw new Error(`No Point core files matched ${DEFAULT_PATTERNS.join(", ")}`);
|
|
180
|
-
const
|
|
181
|
-
const
|
|
214
|
+
const lock = await readPointLock();
|
|
215
|
+
const results = await Promise.all(inputs.map((input) => loadCoreFile(input, lock)));
|
|
216
|
+
const graph = createModuleGraph(results, lock);
|
|
182
217
|
const orderedResults = orderByDependencies(results, graph);
|
|
183
218
|
|
|
184
219
|
if (command === "fmt-all") {
|
|
@@ -223,7 +258,7 @@ async function runProjectCommand(command: string) {
|
|
|
223
258
|
return;
|
|
224
259
|
}
|
|
225
260
|
|
|
226
|
-
if (command === "build-all") {
|
|
261
|
+
if (command === "build-all" || command === "build-js-all") {
|
|
227
262
|
const diagnostics = orderedResults.flatMap((result) =>
|
|
228
263
|
checkPointCore(programWithDependencyDeclarations(result, graph)).map((diagnostic) => ({ ...diagnostic, file: result.input })),
|
|
229
264
|
);
|
|
@@ -232,12 +267,30 @@ async function runProjectCommand(command: string) {
|
|
|
232
267
|
process.exit(1);
|
|
233
268
|
}
|
|
234
269
|
for (const result of orderedResults) {
|
|
235
|
-
const output =
|
|
270
|
+
const output = jsOutputFor(result.input);
|
|
271
|
+
const outputPath = resolve(process.cwd(), output);
|
|
272
|
+
await Bun.$`mkdir -p ${dirname(outputPath)}`.quiet();
|
|
273
|
+
await Bun.write(outputPath, emitPointCoreJavaScript(programWithTypeScriptImports(result, graph)));
|
|
274
|
+
}
|
|
275
|
+
console.log(`Point core JavaScript build wrote ${results.length} files`);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (command === "build-ast-all") {
|
|
280
|
+
const diagnostics = orderedResults.flatMap((result) =>
|
|
281
|
+
checkPointCore(programWithDependencyDeclarations(result, graph)).map((diagnostic) => ({ ...diagnostic, file: result.input })),
|
|
282
|
+
);
|
|
283
|
+
if (diagnostics.length > 0) {
|
|
284
|
+
console.error(JSON.stringify({ ok: false, diagnostics }, null, 2));
|
|
285
|
+
process.exit(1);
|
|
286
|
+
}
|
|
287
|
+
for (const result of orderedResults) {
|
|
288
|
+
const output = astOutputFor(result.input);
|
|
236
289
|
const outputPath = resolve(process.cwd(), output);
|
|
237
290
|
await Bun.$`mkdir -p ${dirname(outputPath)}`.quiet();
|
|
238
291
|
await Bun.write(outputPath, `${JSON.stringify(result.program, null, 2)}\n`);
|
|
239
292
|
}
|
|
240
|
-
console.log(`Point core build wrote ${results.length} files`);
|
|
293
|
+
console.log(`Point core AST build wrote ${results.length} files`);
|
|
241
294
|
return;
|
|
242
295
|
}
|
|
243
296
|
|
|
@@ -259,7 +312,7 @@ async function runProjectCommand(command: string) {
|
|
|
259
312
|
return;
|
|
260
313
|
}
|
|
261
314
|
|
|
262
|
-
if (command === "build-
|
|
315
|
+
if (command === "build-py-all") {
|
|
263
316
|
const diagnostics = orderedResults.flatMap((result) =>
|
|
264
317
|
checkPointCore(programWithDependencyDeclarations(result, graph)).map((diagnostic) => ({ ...diagnostic, file: result.input })),
|
|
265
318
|
);
|
|
@@ -267,13 +320,15 @@ async function runProjectCommand(command: string) {
|
|
|
267
320
|
console.error(JSON.stringify({ ok: false, diagnostics }, null, 2));
|
|
268
321
|
process.exit(1);
|
|
269
322
|
}
|
|
270
|
-
|
|
271
|
-
|
|
323
|
+
const pureLogicResults = orderedResults.filter((result) => isPureLogicProgram(result.program));
|
|
324
|
+
for (const result of pureLogicResults) {
|
|
325
|
+
const output = pyOutputFor(result.input);
|
|
272
326
|
const outputPath = resolve(process.cwd(), output);
|
|
273
327
|
await Bun.$`mkdir -p ${dirname(outputPath)}`.quiet();
|
|
274
|
-
await Bun.write(outputPath,
|
|
328
|
+
await Bun.write(outputPath, emitPointCorePython(programWithTypeScriptImports(result, graph)));
|
|
275
329
|
}
|
|
276
|
-
|
|
330
|
+
const skipped = results.length - pureLogicResults.length;
|
|
331
|
+
console.log(`Point core Python build wrote ${pureLogicResults.length} files${skipped ? ` (${skipped} skipped)` : ""}`);
|
|
277
332
|
return;
|
|
278
333
|
}
|
|
279
334
|
|
|
@@ -358,8 +413,8 @@ async function runPointTests(program: PointCoreProgram, input: string): Promise<
|
|
|
358
413
|
(declaration.semantic?.name.startsWith("test") || declaration.name.startsWith("test")),
|
|
359
414
|
);
|
|
360
415
|
if (tests.length === 0) return { file: input, ok: true, tests: [] };
|
|
361
|
-
const testOutput = resolve(tmpdir(), `point-test-${Date.now()}-${Math.random().toString(16).slice(2)}.
|
|
362
|
-
await Bun.write(testOutput,
|
|
416
|
+
const testOutput = resolve(tmpdir(), `point-test-${Date.now()}-${Math.random().toString(16).slice(2)}.js`);
|
|
417
|
+
await Bun.write(testOutput, emitPointCoreJavaScript(program));
|
|
363
418
|
const mod = await import(pathToFileUrl(testOutput));
|
|
364
419
|
const results = [];
|
|
365
420
|
for (const test of tests) {
|
|
@@ -388,9 +443,9 @@ export function findRunEntryName(program: PointCoreProgram): string | null {
|
|
|
388
443
|
return preferred?.name ?? null;
|
|
389
444
|
}
|
|
390
445
|
|
|
391
|
-
async function loadCoreFile(input: string) {
|
|
446
|
+
async function loadCoreFile(input: string, lock: Awaited<ReturnType<typeof readPointLock>>) {
|
|
392
447
|
const source = await Bun.file(resolve(process.cwd(), input)).text();
|
|
393
|
-
return { input, source, program: parsePointSource(source), uses: parseUseDeclarations(source, input) };
|
|
448
|
+
return { input, source, program: parsePointSource(source), uses: parseUseDeclarations(source, input, lock) };
|
|
394
449
|
}
|
|
395
450
|
|
|
396
451
|
type CoreFile = Awaited<ReturnType<typeof loadCoreFile>>;
|
|
@@ -402,15 +457,15 @@ interface UseDeclaration {
|
|
|
402
457
|
input: string;
|
|
403
458
|
}
|
|
404
459
|
|
|
405
|
-
function parseUseDeclarations(source: string, input: string): UseDeclaration[] {
|
|
460
|
+
function parseUseDeclarations(source: string, input: string, lock: Awaited<ReturnType<typeof readPointLock>>): UseDeclaration[] {
|
|
406
461
|
return source
|
|
407
462
|
.split(/\r?\n/)
|
|
408
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+"([^"]+)")?$/))
|
|
409
464
|
.filter((match): match is RegExpMatchArray => Boolean(match))
|
|
410
|
-
.map((match) => ({ moduleName: match[1]!, from: match[2] ??
|
|
465
|
+
.map((match) => ({ moduleName: match[1]!, from: match[2] ?? modulePathFromLock(lock, match[1]!), input }));
|
|
411
466
|
}
|
|
412
467
|
|
|
413
|
-
function createModuleGraph(results: CoreFile[]): ModuleGraph {
|
|
468
|
+
function createModuleGraph(results: CoreFile[], lock: Awaited<ReturnType<typeof readPointLock>>): ModuleGraph {
|
|
414
469
|
const byInput = new Map(results.map((result) => [normalizeInput(result.input), result]));
|
|
415
470
|
const graph: ModuleGraph = new Map();
|
|
416
471
|
for (const result of results) {
|
|
@@ -474,11 +529,6 @@ function resolveDependencyInput(input: string, from: string): string {
|
|
|
474
529
|
return resolve(base, from).replace(resolve(process.cwd()), "").replace(/^[/\\]/, "");
|
|
475
530
|
}
|
|
476
531
|
|
|
477
|
-
function stdPathFor(moduleName: string): string {
|
|
478
|
-
if (!moduleName.startsWith("std.")) throw new Error(`Use declarations without from must target std modules: ${moduleName}`);
|
|
479
|
-
return `${moduleName.replace(/^std\./, "std/").replaceAll(".", "/")}.point`;
|
|
480
|
-
}
|
|
481
|
-
|
|
482
532
|
function normalizeInput(input: string): string {
|
|
483
533
|
return input.replaceAll("\\", "/");
|
|
484
534
|
}
|
|
@@ -498,6 +548,15 @@ function jsOutputFor(input: string): string {
|
|
|
498
548
|
return `${GENERATED_DIR}/${name}.js`;
|
|
499
549
|
}
|
|
500
550
|
|
|
551
|
+
function astOutputFor(input: string): string {
|
|
552
|
+
return outputFor(input);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function pyOutputFor(input: string): string {
|
|
556
|
+
const name = outputBaseName(input);
|
|
557
|
+
return `${GENERATED_DIR}/${name}.py`;
|
|
558
|
+
}
|
|
559
|
+
|
|
501
560
|
function outputBaseName(input: string): string {
|
|
502
561
|
return normalizeInput(input).split("/").pop()?.replace(/\.point$/, "") ?? "program";
|
|
503
562
|
}
|
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}.`;
|
|
@@ -8,6 +8,8 @@ import type {
|
|
|
8
8
|
PointCoreTypeDeclaration,
|
|
9
9
|
PointCoreValueDeclaration,
|
|
10
10
|
} from "./ast.ts";
|
|
11
|
+
import type { PointSemanticRouteDeclaration } from "../semantic/ast.ts";
|
|
12
|
+
import { semanticFunctionName } from "../semantic/naming.ts";
|
|
11
13
|
|
|
12
14
|
const BINARY_OPERATORS: Record<string, string> = {
|
|
13
15
|
and: "&&",
|
|
@@ -16,13 +18,22 @@ const BINARY_OPERATORS: Record<string, string> = {
|
|
|
16
18
|
|
|
17
19
|
/** Emit JavaScript from a core AST program (no type syntax). Production path: parsePointSource → check → emit. */
|
|
18
20
|
export function emitPointCoreJavaScript(program: PointCoreProgram): string {
|
|
21
|
+
const routes = program.semanticSource?.declarations.filter((declaration): declaration is PointSemanticRouteDeclaration => declaration.kind === "route") ?? [];
|
|
22
|
+
const routeServeCommand = program.declarations.find((declaration) => declaration.kind === "function" && isRouteServeCommand(declaration));
|
|
19
23
|
const lines: string[] = [];
|
|
20
24
|
lines.push("// Generated by Point. Do not edit directly.");
|
|
21
25
|
if (program.module) lines.push(`// Point module: ${program.module}`);
|
|
22
26
|
lines.push("");
|
|
23
27
|
for (const declaration of program.declarations) {
|
|
28
|
+
if (declaration.kind === "function" && declaration.semantic?.kind === "command" && routes.length > 0 && isRouteServeCommand(declaration)) {
|
|
29
|
+
lines.push(...emitRouteServeCommand(declaration), "");
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
24
32
|
lines.push(...emitDeclaration(declaration), "");
|
|
25
33
|
}
|
|
34
|
+
if (routes.length > 0 && routeServeCommand) {
|
|
35
|
+
lines.push(...emitRouteServerRuntime(routes), "");
|
|
36
|
+
}
|
|
26
37
|
return `${trimTrailingBlankLines(lines).join("\n")}\n`;
|
|
27
38
|
}
|
|
28
39
|
|
|
@@ -41,9 +52,13 @@ function emitDeclaration(declaration: PointCoreDeclaration): string[] {
|
|
|
41
52
|
|
|
42
53
|
function emitFunction(declaration: PointCoreFunctionDeclaration): string[] {
|
|
43
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));
|
|
44
59
|
return [
|
|
45
60
|
`export ${asyncPrefix}function ${declaration.name}(${declaration.params.map(emitParam).join(", ")}) {`,
|
|
46
|
-
...indentLines(
|
|
61
|
+
...indentLines(bodyLines),
|
|
47
62
|
"}",
|
|
48
63
|
];
|
|
49
64
|
}
|
|
@@ -122,3 +137,54 @@ function trimTrailingBlankLines(lines: string[]): string[] {
|
|
|
122
137
|
while (lines.at(-1) === "") lines.pop();
|
|
123
138
|
return lines;
|
|
124
139
|
}
|
|
140
|
+
|
|
141
|
+
function isRouteServeCommand(declaration: PointCoreFunctionDeclaration): boolean {
|
|
142
|
+
return declaration.semantic?.name === "serve store readiness" || declaration.name === "serveStoreReadinessCommand";
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function emitRouteServeCommand(declaration: PointCoreFunctionDeclaration): string[] {
|
|
146
|
+
const asyncPrefix = declaration.semantic?.kind === "command" ? "async " : "";
|
|
147
|
+
return [
|
|
148
|
+
`export ${asyncPrefix}function ${declaration.name}(${declaration.params.map(emitParam).join(", ")}) {`,
|
|
149
|
+
" const port = Number(process.env.PORT ?? 3456);",
|
|
150
|
+
" const server = Bun.serve({ port, fetch: createPointRouteFetchHandler() });",
|
|
151
|
+
" console.log(`Store readiness listening on http://localhost:${server.port}`);",
|
|
152
|
+
" await new Promise(() => {});",
|
|
153
|
+
"}",
|
|
154
|
+
];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function emitRouteServerRuntime(routes: PointSemanticRouteDeclaration[]): string[] {
|
|
158
|
+
const matchers = routes.map((route) => {
|
|
159
|
+
const handlerName = semanticFunctionName(route.name, "route", "route");
|
|
160
|
+
const pattern = route.path.replace(/:[A-Za-z][A-Za-z0-9_]*/g, "([^/]+)").replace(/\//g, "\\/");
|
|
161
|
+
const argExpressions = route.inputs.map((_input, index) => `match[${index + 1}]`);
|
|
162
|
+
return {
|
|
163
|
+
method: route.method.toUpperCase(),
|
|
164
|
+
pattern,
|
|
165
|
+
handlerName,
|
|
166
|
+
argExpressions,
|
|
167
|
+
};
|
|
168
|
+
});
|
|
169
|
+
const matchLines = matchers.flatMap((matcher) => [
|
|
170
|
+
` if (req.method === ${JSON.stringify(matcher.method)} && new RegExp(${JSON.stringify(`^${matcher.pattern}$`)}).test(url.pathname)) {`,
|
|
171
|
+
` const match = url.pathname.match(new RegExp(${JSON.stringify(`^${matcher.pattern}$`)}));`,
|
|
172
|
+
` const body = ${matcher.handlerName}(${matcher.argExpressions.join(", ")});`,
|
|
173
|
+
' return new Response(typeof body === "string" ? body : body, { headers: { "content-type": "application/json" } });',
|
|
174
|
+
" }",
|
|
175
|
+
]);
|
|
176
|
+
return [
|
|
177
|
+
"export function createPointRouteFetchHandler() {",
|
|
178
|
+
" return async (req) => {",
|
|
179
|
+
" const url = new URL(req.url);",
|
|
180
|
+
...matchLines,
|
|
181
|
+
' return new Response(JSON.stringify({ error: "Not found" }), { status: 404, headers: { "content-type": "application/json" } });',
|
|
182
|
+
" };",
|
|
183
|
+
"}",
|
|
184
|
+
"",
|
|
185
|
+
"export function startRoutesServer() {",
|
|
186
|
+
" const port = Number(process.env.PORT ?? 3456);",
|
|
187
|
+
" return Bun.serve({ port, fetch: createPointRouteFetchHandler() });",
|
|
188
|
+
"}",
|
|
189
|
+
];
|
|
190
|
+
}
|