bonescript-compiler 0.4.0 → 0.5.0

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/src/ir.ts CHANGED
@@ -169,6 +169,7 @@ export interface IRRelation {
169
169
  to_table: string;
170
170
  foreign_key: string; // column name on the owning side
171
171
  junction_table?: string; // only for many_to_many
172
+ cardinality?: { min: number; max: number | "*" }; // optional explicit bounds
172
173
  }
173
174
 
174
175
  // ─── Invariants ──────────────────────────────────────────────────────────────
@@ -73,6 +73,7 @@ export function lowerChannel(systemName: string, channel: AST.ChannelDeclNode):
73
73
  ordering: channel.ordering || "fifo",
74
74
  persistence: channel.persistence || "none",
75
75
  max_size: channel.maxSize || 10000,
76
+ ...(channel.filter ? { filter: serializeExpr(channel.filter) } : {}),
76
77
  },
77
78
  };
78
79
  }
@@ -1,248 +1,258 @@
1
- /**
2
- * BoneScript Entity Lowering
3
- * Converts EntityDecl + CapabilityDecl AST nodes into IR api_service modules.
4
- * Also handles field lowering and CRUD method generation.
5
- */
6
-
7
- import * as AST from "./ast";
8
- import * as IR from "./ir";
9
- import { makeId, parseDurationMs, serializeType, serializeExpr, toSnakeCase } from "./lowering_helpers";
10
-
11
- // ─── Field Lowering ───────────────────────────────────────────────────────────
12
-
13
- export function lowerField(f: AST.FieldNode): IR.IRField {
14
- const type = serializeType(f.type);
15
- // defaultValue is an ExprNode | null — serialize it to a string if present
16
- const defaultValue = f.defaultValue ? serializeExpr(f.defaultValue) : null;
17
- return {
18
- name: f.name,
19
- type,
20
- nullable: false,
21
- unique: false,
22
- indexed: false,
23
- default_value: defaultValue,
24
- };
25
- }
26
-
27
- // ─── CRUD Method Generation ───────────────────────────────────────────────────
28
-
29
- export function makeCrudMethod(op: string, entityName: string, fields: IR.IRField[]): IR.IRMethod {
30
- const input: IR.IRField[] =
31
- op === "create" || op === "update"
32
- ? fields.filter(f => f.name !== "id" && f.name !== "created_at" && f.name !== "updated_at")
33
- : op === "list"
34
- ? [
35
- { name: "page", type: "uint", nullable: true, unique: false, indexed: false, default_value: "1" },
36
- { name: "page_size", type: "uint", nullable: true, unique: false, indexed: false, default_value: "50" },
37
- ]
38
- : [{ name: "id", type: "uuid", nullable: false, unique: true, indexed: true, default_value: null }];
39
-
40
- return {
41
- name: op,
42
- input,
43
- output: op === "list" ? `list<${entityName}>` : op === "delete" ? "bool" : entityName,
44
- preconditions: [],
45
- effects: [],
46
- emissions: [],
47
- idempotent: op === "read" || op === "list",
48
- authenticated: true,
49
- timeout_ms: 30000,
50
- retry: null,
51
- pipeline: null,
52
- algorithm: null,
53
- sync: null,
54
- };
55
- }
56
-
57
- // ─── Capability Lowering ──────────────────────────────────────────────────────
58
-
59
- export function lowerCapability(cap: AST.CapabilityDeclNode): IR.IRMethod {
60
- const input: IR.IRField[] = cap.params.map(p => ({
61
- name: p.name,
62
- type: serializeType(p.type),
63
- nullable: false,
64
- unique: false,
65
- indexed: false,
66
- default_value: null,
67
- }));
68
-
69
- const preconditions: IR.IRPrecondition[] = cap.requires.map(r => ({
70
- expression: serializeExpr(r),
71
- description: serializeExpr(r),
72
- }));
73
-
74
- const effects: IR.IREffect[] = cap.effects.map(e => ({
75
- target: e.target.path.join("."),
76
- op: e.op === "=" ? "assign" as const : e.op === "+=" ? "add" as const : "remove" as const,
77
- value: serializeExpr(e.value),
78
- }));
79
-
80
- let pipeline: IR.IRPipeline | null = null;
81
- if (cap.pipeline) {
82
- pipeline = {
83
- parallel: cap.pipeline.parallel,
84
- steps: cap.pipeline.steps.map(step => ({
85
- call_name: step.call.name,
86
- call_args: step.call.args.map(a => serializeExpr(a)),
87
- bind_as: step.bindAs,
88
- })),
89
- on_error: cap.pipeline.onError ? {
90
- action: cap.pipeline.onError.action,
91
- call_name: cap.pipeline.onError.call?.name || null,
92
- call_args: cap.pipeline.onError.call?.args.map(a => serializeExpr(a)) || [],
93
- } : null,
94
- };
95
- }
96
-
97
- let algorithm: IR.IRAlgorithm | null = null;
98
- if (cap.algorithm) {
99
- algorithm = {
100
- catalog_name: cap.algorithm.name,
101
- bindings: cap.algorithm.using.map(b => ({
102
- param: b.param,
103
- value: serializeExpr(b.value),
104
- })),
105
- };
106
- }
107
-
108
- return {
109
- name: cap.name,
110
- input,
111
- output: cap.returns ? serializeType(cap.returns) : "result<void, error>",
112
- preconditions,
113
- effects,
114
- emissions: cap.emits.map(e => e.eventName),
115
- idempotent: cap.idempotent || false,
116
- authenticated: true,
117
- timeout_ms: parseDurationMs(cap.timeout) || 30000,
118
- retry: cap.retry ? {
119
- max_attempts: cap.retry.maxAttempts || 3,
120
- backoff: (cap.retry.backoff as IR.IRRetryPolicy["backoff"]) || "exponential",
121
- interval_ms: parseDurationMs(cap.retry.interval) || 1000,
122
- } : null,
123
- pipeline,
124
- algorithm,
125
- sync: cap.sync,
126
- };
127
- }
128
-
129
- // ─── Entity Lowering ──────────────────────────────────────────────────────────
130
-
131
- export function lowerEntity(
132
- systemName: string,
133
- entity: AST.EntityDeclNode,
134
- capabilities: AST.CapabilityDeclNode[],
135
- stores: AST.StoreDeclNode[],
136
- ): IR.IRModule {
137
- const moduleId = makeId(systemName, "api_service", `${entity.name}Service`);
138
-
139
- // Ontology-entailed fields + declared fields
140
- const fields: IR.IRField[] = [
141
- { name: "id", type: "uuid", nullable: false, unique: true, indexed: true, default_value: "gen_random_uuid()" },
142
- { name: "created_at", type: "timestamp", nullable: false, unique: false, indexed: true, default_value: "now()" },
143
- { name: "updated_at", type: "timestamp", nullable: false, unique: false, indexed: false, default_value: "now()" },
144
- ...entity.owns.map(lowerField),
145
- ];
146
-
147
- const derivedFields: IR.IRField[] = entity.derived.map(d => ({
148
- name: d.name,
149
- type: "json",
150
- nullable: true,
151
- unique: false,
152
- indexed: false,
153
- default_value: `GENERATED ALWAYS AS (${serializeExpr(d.expr)}) STORED`,
154
- }));
155
-
156
- const indexes: IR.IRIndex[] = entity.indexes.map(idx => ({ fields: idx, unique: false }));
157
-
158
- const modelConstraints: IR.IRModelConstraint[] = [];
159
- for (const c of entity.constraints) {
160
- const serialized = serializeExpr(c);
161
- if (c.kind === "FieldRef" && c.path[c.path.length - 1] === "unique") {
162
- const field = c.path.slice(0, -1).join(".");
163
- modelConstraints.push({ kind: "unique", target: field, params: {} });
164
- indexes.push({ fields: [field], unique: true });
165
- } else {
166
- modelConstraints.push({ kind: "check", target: entity.name, params: { expression: serialized } });
167
- }
168
- }
169
-
170
- const model: IR.IRModel = {
171
- name: entity.name,
172
- fields: [...fields, ...derivedFields],
173
- primary_key: "id",
174
- indexes,
175
- constraints: modelConstraints,
176
- };
177
-
178
- // State machine
179
- const stateMachines: IR.IRStateMachine[] = [];
180
- if (entity.states) {
181
- const states = entity.states.nodes.map(n => n.name);
182
- const transitions: IR.IRTransition[] = [];
183
- for (const node of entity.states.nodes) {
184
- for (const target of node.transitions) {
185
- transitions.push({ from: node.name, to: target, trigger: `${node.name}_to_${target}`, guard: null });
186
- }
187
- for (const target of node.branches) {
188
- transitions.push({ from: node.name, to: target, trigger: `${node.name}_to_${target}`, guard: null });
189
- }
190
- }
191
- stateMachines.push({ entity: entity.name, states, initial: states[0], transitions });
192
- }
193
-
194
- // Methods: CRUD + capabilities
195
- const methods: IR.IRMethod[] = [
196
- makeCrudMethod("create", entity.name, fields),
197
- makeCrudMethod("read", entity.name, fields),
198
- makeCrudMethod("update", entity.name, fields),
199
- makeCrudMethod("delete", entity.name, fields),
200
- makeCrudMethod("list", entity.name, fields),
201
- ...capabilities.map(lowerCapability),
202
- ];
203
-
204
- // Relations
205
- const relations: IR.IRRelation[] = entity.relations.map(rel => {
206
- const fromTable = toSnakeCase(entity.name) + "s";
207
- const toTable = toSnakeCase(rel.target) + "s";
208
- let foreignKey: string;
209
- let junctionTable: string | undefined;
210
-
211
- switch (rel.relationType) {
212
- case "belongs_to":
213
- foreignKey = toSnakeCase(rel.target) + "_id";
214
- break;
215
- case "has_one":
216
- case "has_many":
217
- foreignKey = toSnakeCase(entity.name) + "_id";
218
- break;
219
- case "many_to_many":
220
- foreignKey = toSnakeCase(entity.name) + "_id";
221
- junctionTable = [fromTable, toTable].sort().join("_");
222
- break;
223
- default:
224
- foreignKey = toSnakeCase(rel.target) + "_id";
225
- }
226
-
227
- return { name: rel.name, kind: rel.relationType, from_entity: entity.name, to_entity: rel.target, from_table: fromTable, to_table: toTable, foreign_key: foreignKey, junction_table: junctionTable };
228
- });
229
-
230
- const relatedStore = stores.find(s => s.name.toLowerCase().includes(entity.name.toLowerCase()));
231
- const deps = relatedStore ? [makeId(systemName, "data_store", relatedStore.name)] : [];
232
-
233
- return {
234
- id: moduleId,
235
- kind: "api_service",
236
- name: `${entity.name}Service`,
237
- interfaces: [{ name: `I${entity.name}Service`, methods }],
238
- models: [model],
239
- events: [],
240
- state_machines: stateMachines,
241
- relations,
242
- dependencies: deps,
243
- config: {
244
- authenticated: entity.auth !== null && entity.auth !== "none",
245
- auth_method: entity.auth || "none",
246
- },
247
- };
248
- }
1
+ /**
2
+ * BoneScript Entity Lowering
3
+ * Converts EntityDecl + CapabilityDecl AST nodes into IR api_service modules.
4
+ * Also handles field lowering and CRUD method generation.
5
+ */
6
+
7
+ import * as AST from "./ast";
8
+ import * as IR from "./ir";
9
+ import { makeId, parseDurationMs, serializeType, serializeExpr, toSnakeCase } from "./lowering_helpers";
10
+
11
+ // ─── Field Lowering ───────────────────────────────────────────────────────────
12
+
13
+ export function lowerField(f: AST.FieldNode): IR.IRField {
14
+ const type = serializeType(f.type);
15
+ // defaultValue is an ExprNode | null — serialize it to a string if present
16
+ const defaultValue = f.defaultValue ? serializeExpr(f.defaultValue) : null;
17
+ return {
18
+ name: f.name,
19
+ type,
20
+ nullable: false,
21
+ unique: false,
22
+ indexed: false,
23
+ default_value: defaultValue,
24
+ };
25
+ }
26
+
27
+ // ─── CRUD Method Generation ───────────────────────────────────────────────────
28
+
29
+ export function makeCrudMethod(op: string, entityName: string, fields: IR.IRField[]): IR.IRMethod {
30
+ const input: IR.IRField[] =
31
+ op === "create" || op === "update"
32
+ ? fields.filter(f => f.name !== "id" && f.name !== "created_at" && f.name !== "updated_at")
33
+ : op === "list"
34
+ ? [
35
+ { name: "page", type: "uint", nullable: true, unique: false, indexed: false, default_value: "1" },
36
+ { name: "page_size", type: "uint", nullable: true, unique: false, indexed: false, default_value: "50" },
37
+ ]
38
+ : [{ name: "id", type: "uuid", nullable: false, unique: true, indexed: true, default_value: null }];
39
+
40
+ return {
41
+ name: op,
42
+ input,
43
+ output: op === "list" ? `list<${entityName}>` : op === "delete" ? "bool" : entityName,
44
+ preconditions: [],
45
+ effects: [],
46
+ emissions: [],
47
+ idempotent: op === "read" || op === "list",
48
+ authenticated: true,
49
+ timeout_ms: 30000,
50
+ retry: null,
51
+ pipeline: null,
52
+ algorithm: null,
53
+ sync: null,
54
+ };
55
+ }
56
+
57
+ // ─── Capability Lowering ──────────────────────────────────────────────────────
58
+
59
+ export function lowerCapability(cap: AST.CapabilityDeclNode): IR.IRMethod {
60
+ const input: IR.IRField[] = cap.params.map(p => ({
61
+ name: p.name,
62
+ type: serializeType(p.type),
63
+ nullable: false,
64
+ unique: false,
65
+ indexed: false,
66
+ default_value: null,
67
+ }));
68
+
69
+ const preconditions: IR.IRPrecondition[] = cap.requires.map(r => ({
70
+ expression: serializeExpr(r),
71
+ description: serializeExpr(r),
72
+ }));
73
+
74
+ const effects: IR.IREffect[] = cap.effects.map(e => ({
75
+ target: e.target.path.join("."),
76
+ op: e.op === "=" ? "assign" as const : e.op === "+=" ? "add" as const : "remove" as const,
77
+ value: serializeExpr(e.value),
78
+ }));
79
+
80
+ let pipeline: IR.IRPipeline | null = null;
81
+ if (cap.pipeline) {
82
+ pipeline = {
83
+ parallel: cap.pipeline.parallel,
84
+ steps: cap.pipeline.steps.map(step => ({
85
+ call_name: step.call.name,
86
+ call_args: step.call.args.map(a => serializeExpr(a)),
87
+ bind_as: step.bindAs,
88
+ })),
89
+ on_error: cap.pipeline.onError ? {
90
+ action: cap.pipeline.onError.action,
91
+ call_name: cap.pipeline.onError.call?.name || null,
92
+ call_args: cap.pipeline.onError.call?.args.map(a => serializeExpr(a)) || [],
93
+ } : null,
94
+ };
95
+ }
96
+
97
+ let algorithm: IR.IRAlgorithm | null = null;
98
+ if (cap.algorithm) {
99
+ algorithm = {
100
+ catalog_name: cap.algorithm.name,
101
+ bindings: cap.algorithm.using.map(b => ({
102
+ param: b.param,
103
+ value: serializeExpr(b.value),
104
+ })),
105
+ };
106
+ }
107
+
108
+ return {
109
+ name: cap.name,
110
+ input,
111
+ output: cap.returns ? serializeType(cap.returns) : "result<void, error>",
112
+ preconditions,
113
+ effects,
114
+ emissions: cap.emits.map(e => e.eventName),
115
+ idempotent: cap.idempotent || false,
116
+ authenticated: true,
117
+ timeout_ms: parseDurationMs(cap.timeout) || 30000,
118
+ retry: cap.retry ? {
119
+ max_attempts: cap.retry.maxAttempts || 3,
120
+ backoff: (cap.retry.backoff as IR.IRRetryPolicy["backoff"]) || "exponential",
121
+ interval_ms: parseDurationMs(cap.retry.interval) || 1000,
122
+ } : null,
123
+ pipeline,
124
+ algorithm,
125
+ sync: cap.sync,
126
+ };
127
+ }
128
+
129
+ // ─── Entity Lowering ──────────────────────────────────────────────────────────
130
+
131
+ export function lowerEntity(
132
+ systemName: string,
133
+ entity: AST.EntityDeclNode,
134
+ capabilities: AST.CapabilityDeclNode[],
135
+ stores: AST.StoreDeclNode[],
136
+ ): IR.IRModule {
137
+ const moduleId = makeId(systemName, "api_service", `${entity.name}Service`);
138
+
139
+ // Ontology-entailed fields + declared fields
140
+ const fields: IR.IRField[] = [
141
+ { name: "id", type: "uuid", nullable: false, unique: true, indexed: true, default_value: "gen_random_uuid()" },
142
+ { name: "created_at", type: "timestamp", nullable: false, unique: false, indexed: true, default_value: "now()" },
143
+ { name: "updated_at", type: "timestamp", nullable: false, unique: false, indexed: false, default_value: "now()" },
144
+ ...entity.owns.map(lowerField),
145
+ ];
146
+
147
+ const derivedFields: IR.IRField[] = entity.derived.map(d => ({
148
+ name: d.name,
149
+ type: "json",
150
+ nullable: true,
151
+ unique: false,
152
+ indexed: false,
153
+ default_value: `GENERATED ALWAYS AS (${serializeExpr(d.expr)}) STORED`,
154
+ }));
155
+
156
+ const indexes: IR.IRIndex[] = entity.indexes.map(idx => ({ fields: idx, unique: false }));
157
+
158
+ const modelConstraints: IR.IRModelConstraint[] = [];
159
+ for (const c of entity.constraints) {
160
+ const serialized = serializeExpr(c);
161
+ if (c.kind === "FieldRef" && c.path[c.path.length - 1] === "unique") {
162
+ const field = c.path.slice(0, -1).join(".");
163
+ modelConstraints.push({ kind: "unique", target: field, params: {} });
164
+ indexes.push({ fields: [field], unique: true });
165
+ } else {
166
+ modelConstraints.push({ kind: "check", target: entity.name, params: { expression: serialized } });
167
+ }
168
+ }
169
+
170
+ const model: IR.IRModel = {
171
+ name: entity.name,
172
+ fields: [...fields, ...derivedFields],
173
+ primary_key: "id",
174
+ indexes,
175
+ constraints: modelConstraints,
176
+ };
177
+
178
+ // State machine
179
+ const stateMachines: IR.IRStateMachine[] = [];
180
+ if (entity.states) {
181
+ const states = entity.states.nodes.map(n => n.name);
182
+ const transitions: IR.IRTransition[] = [];
183
+ for (const node of entity.states.nodes) {
184
+ for (const target of node.transitions) {
185
+ transitions.push({ from: node.name, to: target, trigger: `${node.name}_to_${target}`, guard: null });
186
+ }
187
+ for (const target of node.branches) {
188
+ transitions.push({ from: node.name, to: target, trigger: `${node.name}_to_${target}`, guard: null });
189
+ }
190
+ }
191
+ stateMachines.push({ entity: entity.name, states, initial: states[0], transitions });
192
+ }
193
+
194
+ // Methods: CRUD + capabilities
195
+ const methods: IR.IRMethod[] = [
196
+ makeCrudMethod("create", entity.name, fields),
197
+ makeCrudMethod("read", entity.name, fields),
198
+ makeCrudMethod("update", entity.name, fields),
199
+ makeCrudMethod("delete", entity.name, fields),
200
+ makeCrudMethod("list", entity.name, fields),
201
+ ...capabilities.map(lowerCapability),
202
+ ];
203
+
204
+ // Relations
205
+ const relations: IR.IRRelation[] = entity.relations.map(rel => {
206
+ const fromTable = toSnakeCase(entity.name) + "s";
207
+ const toTable = toSnakeCase(rel.target) + "s";
208
+ let foreignKey: string;
209
+ let junctionTable: string | undefined;
210
+
211
+ switch (rel.relationType) {
212
+ case "belongs_to":
213
+ foreignKey = toSnakeCase(rel.target) + "_id";
214
+ break;
215
+ case "has_one":
216
+ case "has_many":
217
+ foreignKey = toSnakeCase(entity.name) + "_id";
218
+ break;
219
+ case "many_to_many":
220
+ foreignKey = toSnakeCase(entity.name) + "_id";
221
+ junctionTable = [fromTable, toTable].sort().join("_");
222
+ break;
223
+ default:
224
+ foreignKey = toSnakeCase(rel.target) + "_id";
225
+ }
226
+
227
+ return {
228
+ name: rel.name,
229
+ kind: rel.relationType,
230
+ from_entity: entity.name,
231
+ to_entity: rel.target,
232
+ from_table: fromTable,
233
+ to_table: toTable,
234
+ foreign_key: foreignKey,
235
+ junction_table: junctionTable,
236
+ cardinality: rel.cardinality ?? undefined,
237
+ };
238
+ });
239
+
240
+ const relatedStore = stores.find(s => s.name.toLowerCase().includes(entity.name.toLowerCase()));
241
+ const deps = relatedStore ? [makeId(systemName, "data_store", relatedStore.name)] : [];
242
+
243
+ return {
244
+ id: moduleId,
245
+ kind: "api_service",
246
+ name: `${entity.name}Service`,
247
+ interfaces: [{ name: `I${entity.name}Service`, methods }],
248
+ models: [model],
249
+ events: [],
250
+ state_machines: stateMachines,
251
+ relations,
252
+ dependencies: deps,
253
+ config: {
254
+ authenticated: entity.auth !== null && entity.auth !== "none",
255
+ auth_method: entity.auth || "none",
256
+ },
257
+ };
258
+ }
package/src/optimizer.ts CHANGED
@@ -52,7 +52,7 @@ function deadModuleElimination(s: IR.IRSystem, log: OptimizationLog[]): IR.IRSys
52
52
 
53
53
  // Seed: always-reachable kinds
54
54
  for (const m of s.modules) {
55
- if (["gateway", "frontend", "auth_service", "api_service", "realtime_service"].includes(m.kind)) {
55
+ if (["gateway", "frontend", "auth_service", "api_service", "realtime_service", "data_store", "event_bus", "cache"].includes(m.kind)) {
56
56
  reachable.add(m.id);
57
57
  }
58
58
  }
package/src/scaffold.ts CHANGED
@@ -163,7 +163,6 @@ const TEMPLATES: Record<ScaffoldDomain, string> = {
163
163
  }
164
164
 
165
165
  store DeviceStore {
166
- engine: dynamodb
167
166
  schema: {
168
167
  id: uuid,
169
168
  serial: string,