@hatchingpoint/point 0.0.5 → 0.0.6

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.
@@ -0,0 +1,347 @@
1
+ import type { PointSourceSpan } from "../core/ast.ts";
2
+ import { lexPointCore } from "../core/lexer.ts";
3
+ import type {
4
+ PointSemanticBinaryOperator,
5
+ PointSemanticExpression,
6
+ PointSemanticRecordLiteralField,
7
+ PointSemanticTypeExpression,
8
+ } from "./ast.ts";
9
+
10
+ export interface PointSemanticExpressionContext {
11
+ bindings: string[];
12
+ atoms: string[];
13
+ callables: string[];
14
+ recordFields: Map<string, string[]>;
15
+ }
16
+
17
+ export function parseSemanticTypeExpression(source: string): PointSemanticTypeExpression {
18
+ const trimmed = source.trim();
19
+ const orParts = splitTopLevel(trimmed, " or ");
20
+ if (orParts.length > 1) {
21
+ return {
22
+ kind: "typeRef",
23
+ name: "Or",
24
+ args: orParts.map(parseSemanticTypeExpression),
25
+ };
26
+ }
27
+ const listMatch = trimmed.match(/^List<(.+)>$/);
28
+ if (listMatch) {
29
+ return { kind: "typeRef", name: "List", args: [parseSemanticTypeExpression(listMatch[1] ?? "")] };
30
+ }
31
+ const maybeMatch = trimmed.match(/^Maybe<(.+)>$/);
32
+ if (maybeMatch) {
33
+ return { kind: "typeRef", name: "Maybe", args: [parseSemanticTypeExpression(maybeMatch[1] ?? "")] };
34
+ }
35
+ return { kind: "typeRef", name: trimmed, args: [] };
36
+ }
37
+
38
+ export function parseSemanticExpression(
39
+ source: string,
40
+ context: PointSemanticExpressionContext,
41
+ span?: PointSourceSpan,
42
+ ): PointSemanticExpression {
43
+ const errorMatch = source.trim().match(/^Error\s+("(?:\\.|[^"\\])*")$/);
44
+ if (errorMatch) {
45
+ const expression: PointSemanticExpression = { kind: "error", message: JSON.parse(errorMatch[1] ?? '""') as string };
46
+ return span ? withExpressionSpan(expression, span) : expression;
47
+ }
48
+ const expression = parseBinaryExpression(source.trim(), 0, context).expression;
49
+ return span ? withExpressionSpan(expression, span) : expression;
50
+ }
51
+
52
+ export function withExpressionSpan(expression: PointSemanticExpression, span: PointSourceSpan): PointSemanticExpression {
53
+ switch (expression.kind) {
54
+ case "literal":
55
+ case "name":
56
+ case "error":
57
+ return { ...expression, span: expression.span ?? span };
58
+ case "property":
59
+ return {
60
+ ...expression,
61
+ span: expression.span ?? span,
62
+ target: withExpressionSpan(expression.target, span),
63
+ };
64
+ case "binary":
65
+ return {
66
+ ...expression,
67
+ span: expression.span ?? span,
68
+ left: withExpressionSpan(expression.left, span),
69
+ right: withExpressionSpan(expression.right, span),
70
+ };
71
+ case "call":
72
+ return {
73
+ ...expression,
74
+ span: expression.span ?? span,
75
+ args: expression.args.map((arg) => withExpressionSpan(arg, span)),
76
+ };
77
+ case "await":
78
+ return {
79
+ ...expression,
80
+ span: expression.span ?? span,
81
+ value: withExpressionSpan(expression.value, span),
82
+ };
83
+ case "list":
84
+ return {
85
+ ...expression,
86
+ span: expression.span ?? span,
87
+ items: expression.items.map((item) => withExpressionSpan(item, span)),
88
+ };
89
+ case "record":
90
+ return {
91
+ ...expression,
92
+ span: expression.span ?? span,
93
+ fields: expression.fields.map((field) => ({
94
+ ...field,
95
+ value: withExpressionSpan(field.value, span),
96
+ })),
97
+ };
98
+ }
99
+ }
100
+
101
+ function parseBinaryExpression(
102
+ source: string,
103
+ minPrecedence: number,
104
+ context: PointSemanticExpressionContext,
105
+ ): { expression: PointSemanticExpression; consumed: string } {
106
+ let left = parsePrimaryExpression(source, context);
107
+ let rest = left.consumed.trimStart();
108
+ while (rest) {
109
+ const operator = peekBinaryOperator(rest);
110
+ if (!operator) break;
111
+ const precedence = precedenceFor(operator.op);
112
+ if (precedence < minPrecedence) break;
113
+ const right = parseBinaryExpression(operator.rest.trimStart(), precedence + 1, context);
114
+ left = {
115
+ expression: {
116
+ kind: "binary",
117
+ operator: operator.op,
118
+ left: left.expression,
119
+ right: right.expression,
120
+ },
121
+ consumed: right.consumed,
122
+ };
123
+ rest = left.consumed.trimStart();
124
+ }
125
+ return left;
126
+ }
127
+
128
+ function parsePrimaryExpression(source: string, context: PointSemanticExpressionContext): { expression: PointSemanticExpression; consumed: string } {
129
+ const trimmed = source.trim();
130
+ if (trimmed.startsWith("await ")) {
131
+ const inner = parsePrimaryExpression(trimmed.slice("await ".length), context);
132
+ return { expression: { kind: "await", value: inner.expression }, consumed: inner.consumed };
133
+ }
134
+ if (trimmed.startsWith("[")) {
135
+ return parseListExpression(trimmed, context);
136
+ }
137
+ if (trimmed.startsWith("{")) {
138
+ return parseRecordExpression(trimmed, context);
139
+ }
140
+ if (trimmed.startsWith('"')) {
141
+ const end = trimmed.indexOf('"', 1);
142
+ const value = JSON.parse(trimmed.slice(0, end + 1));
143
+ return { expression: { kind: "literal", value }, consumed: trimmed.slice(end + 1) };
144
+ }
145
+ if (/^(true|false|null|none)\b/.test(trimmed)) {
146
+ const match = trimmed.match(/^(true|false|null|none)\b/);
147
+ const token = match?.[1];
148
+ const value = token === "true" ? true : token === "false" ? false : null;
149
+ return { expression: { kind: "literal", value }, consumed: trimmed.slice(match?.[0].length ?? 0) };
150
+ }
151
+ if (/^\d/.test(trimmed)) {
152
+ const match = trimmed.match(/^(\d+(?:\.\d+)?)/);
153
+ const value = Number(match?.[1] ?? 0);
154
+ return { expression: { kind: "literal", value }, consumed: trimmed.slice(match?.[0].length ?? 0) };
155
+ }
156
+ const callMatch = matchCall(trimmed, context);
157
+ if (callMatch) return callMatch;
158
+ const atom = matchAtom(trimmed, context);
159
+ if (atom) {
160
+ if (atom.includes(".")) {
161
+ const dot = atom.indexOf(".");
162
+ return {
163
+ expression: {
164
+ kind: "property",
165
+ target: { kind: "name", label: atom.slice(0, dot) },
166
+ label: atom.slice(dot + 1),
167
+ },
168
+ consumed: trimmed.slice(atom.length),
169
+ };
170
+ }
171
+ let expression: PointSemanticExpression = { kind: "name", label: atom };
172
+ let consumed = trimmed.slice(atom.length);
173
+ while (consumed.trimStart().startsWith(".")) {
174
+ let rest = consumed.trimStart().slice(1);
175
+ const fieldMatch = rest.match(/^([A-Za-z][A-Za-z0-9 ]*[A-Za-z0-9]|[A-Za-z])/);
176
+ if (!fieldMatch) break;
177
+ const label = fieldMatch[0] ?? "";
178
+ expression = { kind: "property", target: expression, label };
179
+ consumed = rest.slice(label.length);
180
+ }
181
+ return { expression, consumed };
182
+ }
183
+ throw new Error(`Unable to parse semantic expression: ${source}`);
184
+ }
185
+
186
+ function parseListExpression(source: string, context: PointSemanticExpressionContext): { expression: PointSemanticExpression; consumed: string } {
187
+ let rest = source.trim().slice(1).trimStart();
188
+ const items: PointSemanticExpression[] = [];
189
+ while (rest && !rest.startsWith("]")) {
190
+ const parsed = parseBinaryExpression(rest, 0, context);
191
+ items.push(parsed.expression);
192
+ rest = parsed.consumed.trimStart();
193
+ if (rest.startsWith(",")) rest = rest.slice(1).trimStart();
194
+ }
195
+ if (!rest.startsWith("]")) throw new Error(`Unterminated list literal: ${source}`);
196
+ return { expression: { kind: "list", items }, consumed: rest.slice(1) };
197
+ }
198
+
199
+ function parseRecordExpression(source: string, context: PointSemanticExpressionContext): { expression: PointSemanticExpression; consumed: string } {
200
+ let rest = source.trim().slice(1).trimStart();
201
+ const fields: PointSemanticRecordLiteralField[] = [];
202
+ while (rest && !rest.startsWith("}")) {
203
+ const colon = rest.indexOf(":");
204
+ if (colon === -1) throw new Error(`Expected record field label: ${source}`);
205
+ const label = rest.slice(0, colon).trim();
206
+ rest = rest.slice(colon + 1).trimStart();
207
+ const parsed = parseBinaryExpression(rest, 0, context);
208
+ fields.push({ label, value: parsed.expression });
209
+ rest = parsed.consumed.trimStart();
210
+ if (rest.startsWith(",")) rest = rest.slice(1).trimStart();
211
+ }
212
+ if (!rest.startsWith("}")) throw new Error(`Unterminated record literal: ${source}`);
213
+ return { expression: { kind: "record", fields }, consumed: rest.slice(1) };
214
+ }
215
+
216
+ function matchCall(source: string, context: PointSemanticExpressionContext): { expression: PointSemanticExpression; consumed: string } | null {
217
+ const candidates = [...context.callables, ...context.atoms, ...context.bindings].sort((a, b) => b.length - a.length);
218
+ for (const callee of candidates) {
219
+ const parsed = parseCallExpression(source, callee, context);
220
+ if (parsed) return parsed;
221
+ }
222
+ const generic = source.match(/^([A-Za-z_][A-Za-z0-9_]*)\(/);
223
+ if (generic) return parseCallExpression(source, generic[1] ?? "", context);
224
+ return null;
225
+ }
226
+
227
+ function parseCallExpression(
228
+ source: string,
229
+ callee: string,
230
+ context: PointSemanticExpressionContext,
231
+ ): { expression: PointSemanticExpression; consumed: string } | null {
232
+ if (!source.startsWith(callee)) return null;
233
+ const after = source.slice(callee.length);
234
+ if (!after.startsWith("(")) return null;
235
+ let depth = 0;
236
+ let index = 0;
237
+ for (; index < after.length; index += 1) {
238
+ const char = after[index];
239
+ if (char === "(") depth += 1;
240
+ if (char === ")") {
241
+ depth -= 1;
242
+ if (depth === 0) break;
243
+ }
244
+ }
245
+ const argSource = after.slice(1, index);
246
+ const args = argSource.trim() ? splitTopLevel(argSource, ",").map((part) => parseSemanticExpression(part, context)) : [];
247
+ return {
248
+ expression: { kind: "call", callee, args },
249
+ consumed: source.slice(callee.length + index + 1),
250
+ };
251
+ }
252
+
253
+ function matchAtom(source: string, context: PointSemanticExpressionContext): string | null {
254
+ for (const atom of [...context.atoms, ...context.bindings].sort((a, b) => b.length - a.length)) {
255
+ if (!source.startsWith(atom)) continue;
256
+ const next = source[atom.length];
257
+ if (next && /[A-Za-z0-9_]/.test(next)) continue;
258
+ return atom;
259
+ }
260
+ return null;
261
+ }
262
+
263
+ function peekBinaryOperator(source: string): { op: PointSemanticBinaryOperator; rest: string } | null {
264
+ for (const op of ["==", "!=", "<=", ">=", "and", "or", "+", "-", "*", "/", "<", ">"] as const) {
265
+ if (source.startsWith(op)) {
266
+ const next = source[op.length];
267
+ if (op === "-" && /^\d/.test(source)) return null;
268
+ if (next && /[A-Za-z0-9_]/.test(next) && !["and", "or"].includes(op)) continue;
269
+ return { op, rest: source.slice(op.length) };
270
+ }
271
+ }
272
+ return null;
273
+ }
274
+
275
+ function precedenceFor(operator: PointSemanticBinaryOperator): number {
276
+ if (operator === "or") return 1;
277
+ if (operator === "and") return 2;
278
+ if (operator === "==" || operator === "!=") return 3;
279
+ if (operator === "<" || operator === "<=" || operator === ">" || operator === ">=") return 4;
280
+ if (operator === "+" || operator === "-") return 5;
281
+ return 6;
282
+ }
283
+
284
+ function splitTopLevel(source: string, separator: string): string[] {
285
+ const parts: string[] = [];
286
+ let depth = 0;
287
+ let quote: '"' | null = null;
288
+ let current = "";
289
+ for (let index = 0; index < source.length; index += 1) {
290
+ const char = source[index];
291
+ if (quote) {
292
+ current += char;
293
+ if (char === quote && source[index - 1] !== "\\") quote = null;
294
+ continue;
295
+ }
296
+ if (char === '"') {
297
+ quote = '"';
298
+ current += char;
299
+ continue;
300
+ }
301
+ if (char === "<" || char === "(" || char === "[") depth += 1;
302
+ if (char === ">" || char === ")" || char === "]") depth -= 1;
303
+ if (depth === 0 && source.slice(index, index + separator.length) === separator) {
304
+ parts.push(current.trim());
305
+ current = "";
306
+ index += separator.length - 1;
307
+ continue;
308
+ }
309
+ current += char;
310
+ }
311
+ if (current.trim()) parts.push(current.trim());
312
+ return parts;
313
+ }
314
+
315
+ export function buildExpressionContext(options: {
316
+ bindings?: string[];
317
+ paramTypes?: Map<string, string>;
318
+ recordFields?: Map<string, Map<string, string>>;
319
+ callables?: string[];
320
+ }): PointSemanticExpressionContext {
321
+ const bindings = options.bindings ?? [];
322
+ const callables = options.callables ?? [];
323
+ const atoms: string[] = [...bindings];
324
+ const recordFields = new Map<string, string[]>();
325
+ for (const [param, type] of options.paramTypes ?? []) {
326
+ const fields = options.recordFields?.get(type);
327
+ if (!fields) continue;
328
+ recordFields.set(type, [...fields.keys()]);
329
+ for (const label of fields.keys()) atoms.push(`${param}.${label}`);
330
+ }
331
+ return { bindings, atoms, callables, recordFields };
332
+ }
333
+
334
+ export function lineSpan(source: string, lineNumber: number): PointSourceSpan {
335
+ const lines = source.split(/\r?\n/);
336
+ let offset = 0;
337
+ for (let index = 0; index < lineNumber - 1; index += 1) offset += (lines[index]?.length ?? 0) + 1;
338
+ const line = lines[lineNumber - 1] ?? "";
339
+ return {
340
+ start: { line: lineNumber, column: 1, offset },
341
+ end: { line: lineNumber, column: line.length + 1, offset: offset + line.length },
342
+ };
343
+ }
344
+
345
+ export function validateExpressionWithLexer(source: string): void {
346
+ lexPointCore(source.replace(/\band\b/g, "&&").replace(/\bor\b/g, "||"));
347
+ }
@@ -0,0 +1,222 @@
1
+ import type {
2
+ PointSemanticBinding,
3
+ PointSemanticCalculationStatement,
4
+ PointSemanticCommandStatement,
5
+ PointSemanticDeclaration,
6
+ PointSemanticExpression,
7
+ PointSemanticLabelStatement,
8
+ PointSemanticMutationStatement,
9
+ PointSemanticOutputBinding,
10
+ PointSemanticPolicyStatement,
11
+ PointSemanticProgram,
12
+ PointSemanticRouteStatement,
13
+ PointSemanticRuleStatement,
14
+ PointSemanticTypeExpression,
15
+ PointSemanticViewStatement,
16
+ PointSemanticWorkflowStatement,
17
+ PointSemanticActionStatement,
18
+ } from "./ast.ts";
19
+
20
+ export function formatSemanticProgram(program: PointSemanticProgram): string {
21
+ const blocks: string[] = [];
22
+ if (program.module) blocks.push(`module ${program.module}`);
23
+ for (const use of program.uses) {
24
+ blocks.push(use.from ? `use ${use.moduleName} from ${JSON.stringify(use.from)}` : `use ${use.moduleName}`);
25
+ }
26
+ for (const declaration of program.declarations) {
27
+ blocks.push(formatDeclaration(declaration).join("\n"));
28
+ }
29
+ return `${blocks.join("\n\n")}\n`;
30
+ }
31
+
32
+ function formatDeclaration(declaration: PointSemanticDeclaration): string[] {
33
+ switch (declaration.kind) {
34
+ case "record":
35
+ return [`record ${declaration.name}`, ...declaration.fields.map((field) => ` ${field.label}: ${formatType(field.type)}`)];
36
+ case "external":
37
+ return [
38
+ `external ${declaration.name}`,
39
+ ...declaration.functions.map(
40
+ (fn) =>
41
+ ` ${fn.label}(${fn.params.map(formatBinding).join(", ")}): ${formatType(fn.returnType)} from ${JSON.stringify(fn.from)}${fn.importAs ? ` as ${fn.importAs}` : ""}`,
42
+ ),
43
+ ];
44
+ case "calculation":
45
+ return [
46
+ `calculation ${declaration.name}`,
47
+ ...formatInputs(declaration.inputs),
48
+ ...formatOutput(declaration.output, declaration.kind),
49
+ ...declaration.body.map((statement) => ` ${formatCalculationStatement(statement)}`),
50
+ ];
51
+ case "rule":
52
+ return [
53
+ `rule ${declaration.name}`,
54
+ ...formatInputs(declaration.inputs),
55
+ ...formatOutput(declaration.output, declaration.kind),
56
+ ...declaration.body.flatMap((statement) => formatRuleStatement(statement)),
57
+ ];
58
+ case "label":
59
+ return [
60
+ `label ${declaration.name}`,
61
+ ...formatInputs(declaration.inputs),
62
+ ...formatOutput(declaration.output, declaration.kind),
63
+ ...declaration.body.map((statement) => ` ${formatLabelStatement(statement)}`),
64
+ ];
65
+ case "action":
66
+ return [
67
+ `action ${declaration.name}`,
68
+ ...formatInputs(declaration.inputs),
69
+ ...formatOutput(declaration.output, declaration.kind),
70
+ ...(declaration.touches.length > 0 ? [` touches ${declaration.touches.join(", ")}`] : []),
71
+ ...declaration.body.map((statement) => ` ${formatActionStatement(statement)}`),
72
+ ];
73
+ case "policy":
74
+ return [
75
+ `policy ${declaration.name}`,
76
+ ...formatInputs(declaration.inputs),
77
+ ...declaration.body.map((statement) => ` ${formatPolicyStatement(statement)}`),
78
+ ];
79
+ case "view":
80
+ return [
81
+ `view ${declaration.name}`,
82
+ ...formatInputs(declaration.inputs),
83
+ ...formatViewOutput(declaration.output),
84
+ ...declaration.body.map((statement) => ` ${formatViewStatement(statement)}`),
85
+ ];
86
+ case "route":
87
+ return [
88
+ `route ${declaration.name}`,
89
+ ` method ${declaration.method}`,
90
+ ` path ${JSON.stringify(declaration.path)}`,
91
+ ...formatInputs(declaration.inputs).map((line) => ` ${line.trimStart()}`),
92
+ ...formatOutput(declaration.output, declaration.kind).map((line) => ` ${line.trimStart()}`),
93
+ ...declaration.body.map((statement) => ` ${formatRouteStatement(statement)}`),
94
+ ];
95
+ case "workflow":
96
+ return [
97
+ `workflow ${declaration.name}`,
98
+ ...formatInputs(declaration.inputs),
99
+ ...formatOutput(declaration.output, declaration.kind),
100
+ ...declaration.body.map((statement) => ` ${formatWorkflowStatement(statement)}`),
101
+ ];
102
+ case "command":
103
+ return [
104
+ `command ${declaration.name}`,
105
+ ...formatInputs(declaration.inputs),
106
+ ...formatOutput(declaration.output, declaration.kind),
107
+ ...declaration.body.map((statement) => ` ${formatCommandStatement(statement)}`),
108
+ ];
109
+ }
110
+ }
111
+
112
+ function formatInputs(inputs: PointSemanticBinding[]): string[] {
113
+ return inputs.map((input) => ` input ${formatBinding(input)}`);
114
+ }
115
+
116
+ function formatBinding(binding: PointSemanticBinding): string {
117
+ return `${binding.label}: ${formatType(binding.type)}`;
118
+ }
119
+
120
+ function formatOutput(output: PointSemanticOutputBinding, kind: PointSemanticDeclaration["kind"]): string[] {
121
+ if (kind === "label" && output.name === "result") return [` output ${formatType(output.type)}`];
122
+ if (kind === "policy") return [];
123
+ if (kind === "view" && output.name === "page" && output.type.name === "Page") return [];
124
+ if (output.type.name === "Void" && output.name === "result") {
125
+ if (kind === "action" || kind === "command") return [" output Void"];
126
+ if (kind === "calculation" || kind === "rule" || kind === "workflow") return [];
127
+ }
128
+ return [` output ${output.name}: ${formatType(output.type)}`];
129
+ }
130
+
131
+ function formatViewOutput(output: PointSemanticOutputBinding): string[] {
132
+ if (output.name === "page" && output.type.name === "Page") return [];
133
+ return formatOutput(output, "view");
134
+ }
135
+
136
+ function formatCalculationStatement(statement: PointSemanticCalculationStatement): string {
137
+ if (statement.kind === "assignIs") return `${statement.name} is ${formatExpression(statement.value)}`;
138
+ if (statement.kind === "startsAt") return `${statement.name} starts at ${formatExpression(statement.value)}`;
139
+ if (statement.kind === "startsAs") return `${statement.name} starts as ${formatExpression(statement.value)}`;
140
+ if (statement.kind === "forEach") {
141
+ return [`for each ${statement.item} in ${formatExpression(statement.iterable)}`, ...statement.body.map((mutation) => ` ${formatMutation(mutation)}`)].join(
142
+ "\n",
143
+ );
144
+ }
145
+ if (statement.kind === "return") return `return ${formatExpression(statement.value)}`;
146
+ return formatMutation(statement);
147
+ }
148
+
149
+ function formatRuleStatement(statement: PointSemanticRuleStatement): string[] {
150
+ if (statement.kind === "startsAt") return [` ${statement.name} starts at ${formatExpression(statement.value)}`];
151
+ if (statement.kind === "addWhen") return [` add ${formatExpression(statement.amount)} when ${formatExpression(statement.condition)}`];
152
+ if (statement.kind === "forEach") {
153
+ return [
154
+ ` for each ${statement.item} in ${formatExpression(statement.iterable)}`,
155
+ ...statement.body.map((mutation) => ` ${formatMutation(mutation)}`),
156
+ ];
157
+ }
158
+ if (statement.kind === "return") return [` return ${formatExpression(statement.value)}`];
159
+ return [` ${formatMutation(statement)}`];
160
+ }
161
+
162
+ function formatLabelStatement(statement: PointSemanticLabelStatement): string {
163
+ if (statement.kind === "whenReturn") return `when ${formatExpression(statement.condition)} return ${formatExpression(statement.value)}`;
164
+ return `otherwise return ${formatExpression(statement.value)}`;
165
+ }
166
+
167
+ function formatActionStatement(statement: PointSemanticActionStatement): string {
168
+ return `return ${formatExpression(statement.value)}`;
169
+ }
170
+
171
+ function formatPolicyStatement(statement: PointSemanticPolicyStatement): string {
172
+ if (statement.kind === "deny") return `deny ${formatExpression(statement.condition)}`;
173
+ if (statement.kind === "require") return `require ${formatExpression(statement.condition)}`;
174
+ return `allow ${formatExpression(statement.condition)}`;
175
+ }
176
+
177
+ function formatViewStatement(statement: PointSemanticViewStatement): string {
178
+ if (statement.kind === "whenRender") return `when ${formatExpression(statement.condition)} render ${formatExpression(statement.value)}`;
179
+ return `render ${formatExpression(statement.value)}`;
180
+ }
181
+
182
+ function formatRouteStatement(statement: PointSemanticRouteStatement): string {
183
+ return `return ${formatExpression(statement.value)}`;
184
+ }
185
+
186
+ function formatWorkflowStatement(statement: PointSemanticWorkflowStatement): string {
187
+ if (statement.kind === "step") return `step ${statement.name} is ${formatExpression(statement.value)}`;
188
+ return `return ${formatExpression(statement.value)}`;
189
+ }
190
+
191
+ function formatCommandStatement(statement: PointSemanticCommandStatement): string {
192
+ return `return ${formatExpression(statement.value)}`;
193
+ }
194
+
195
+ function formatMutation(statement: PointSemanticMutationStatement): string {
196
+ if (statement.kind === "addTo") return `add ${formatExpression(statement.amount)} to ${statement.target}`;
197
+ if (statement.kind === "subtractFrom") return `subtract ${formatExpression(statement.amount)} from ${statement.target}`;
198
+ return `set ${statement.target} to ${formatExpression(statement.value)}`;
199
+ }
200
+
201
+ function formatExpression(expression: PointSemanticExpression): string {
202
+ if (expression.kind === "literal") {
203
+ if (expression.value === null) return "none";
204
+ return JSON.stringify(expression.value);
205
+ }
206
+ if (expression.kind === "name") return expression.label;
207
+ if (expression.kind === "property") return `${formatExpression(expression.target)}.${expression.label}`;
208
+ if (expression.kind === "binary") return `${formatExpression(expression.left)} ${expression.operator} ${formatExpression(expression.right)}`;
209
+ if (expression.kind === "call") return `${expression.callee}(${expression.args.map(formatExpression).join(", ")})`;
210
+ if (expression.kind === "await") return `await ${formatExpression(expression.value)}`;
211
+ if (expression.kind === "list") return `[${expression.items.map(formatExpression).join(", ")}]`;
212
+ if (expression.kind === "record") {
213
+ return `{ ${expression.fields.map((field) => `${field.label}: ${formatExpression(field.value)}`).join(", ")} }`;
214
+ }
215
+ return `Error ${JSON.stringify(expression.message)}`;
216
+ }
217
+
218
+ function formatType(type: PointSemanticTypeExpression): string {
219
+ if (type.name === "Or") return type.args.map(formatType).join(" or ");
220
+ if (type.args.length === 0) return type.name;
221
+ return `${type.name}<${type.args.map(formatType).join(", ")}>`;
222
+ }
@@ -0,0 +1,10 @@
1
+ export * from "./ast.ts";
2
+ export * from "./callables.ts";
3
+ export * from "./context.ts";
4
+ export * from "./desugar.ts";
5
+ export * from "./expressions.ts";
6
+ export * from "./format.ts";
7
+ export * from "./metadata.ts";
8
+ export * from "./naming.ts";
9
+ export * from "./parse.ts";
10
+ export * from "./serialize.ts";
@@ -0,0 +1,37 @@
1
+ import type { PointSemanticDeclarationMetadata } from "../core/ast.ts";
2
+ import type { PointSemanticDeclaration } from "./ast.ts";
3
+
4
+ export function semanticDeclarationMetadata(declaration: PointSemanticDeclaration): PointSemanticDeclarationMetadata {
5
+ if (declaration.kind === "record") {
6
+ return { kind: "record", name: declaration.name, outputName: undefined, effects: undefined };
7
+ }
8
+ if (declaration.kind === "external") {
9
+ return { kind: "external", name: declaration.name, outputName: undefined, effects: undefined };
10
+ }
11
+ if (declaration.kind === "calculation" || declaration.kind === "rule" || declaration.kind === "action" || declaration.kind === "workflow" || declaration.kind === "command") {
12
+ return {
13
+ kind: declaration.kind,
14
+ name: declaration.name,
15
+ outputName: declaration.output.name,
16
+ effects: declaration.kind === "action" ? declaration.touches : [],
17
+ };
18
+ }
19
+ if (declaration.kind === "label") {
20
+ return { kind: "label", name: declaration.name, outputName: declaration.output.name, effects: [] };
21
+ }
22
+ if (declaration.kind === "policy") {
23
+ return { kind: "policy", name: declaration.name, outputName: "policy", effects: [] };
24
+ }
25
+ if (declaration.kind === "view") {
26
+ return {
27
+ kind: "view",
28
+ name: declaration.name,
29
+ outputName: declaration.output.name === "page" ? "view" : declaration.output.name,
30
+ effects: [],
31
+ };
32
+ }
33
+ if (declaration.kind === "route") {
34
+ return { kind: "route", name: declaration.name, outputName: declaration.output.name, effects: [] };
35
+ }
36
+ throw new Error(`Unsupported semantic metadata for ${declaration.kind}`);
37
+ }
@@ -0,0 +1,33 @@
1
+ export function toPascalCase(label: string): string {
2
+ const words = label.match(/[A-Za-z0-9]+/g) ?? [];
3
+ return words.map((word) => `${word.slice(0, 1).toUpperCase()}${word.slice(1)}`).join("");
4
+ }
5
+
6
+ export function toIdentifier(label: string): string {
7
+ const words = label.match(/[A-Za-z0-9]+/g) ?? [];
8
+ return words.map((word, index) => (index === 0 ? word.toLowerCase() : toPascalCase(word))).join("");
9
+ }
10
+
11
+ export function semanticFunctionName(
12
+ label: string,
13
+ outputName: string,
14
+ kind: "calculation" | "rule" | "label" | "action" | "policy" | "view" | "route" | "workflow" | "command",
15
+ ): string {
16
+ const base = toIdentifier(label);
17
+ const suffix =
18
+ kind === "label"
19
+ ? "Label"
20
+ : kind === "policy"
21
+ ? "Policy"
22
+ : kind === "view"
23
+ ? "View"
24
+ : kind === "route"
25
+ ? "Route"
26
+ : kind === "workflow"
27
+ ? "Workflow"
28
+ : kind === "command"
29
+ ? "Command"
30
+ : toPascalCase(outputName);
31
+ if (!suffix) return base;
32
+ return base.toLowerCase().endsWith(suffix.toLowerCase()) ? base : `${base}${suffix}`;
33
+ }