bonescript-compiler 0.2.1 → 0.4.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.
Files changed (167) hide show
  1. package/LICENSE +21 -21
  2. package/dist/algorithm_catalog.js +166 -166
  3. package/dist/cli.d.ts +2 -1
  4. package/dist/cli.js +75 -543
  5. package/dist/cli.js.map +1 -1
  6. package/dist/commands/check.d.ts +5 -0
  7. package/dist/commands/check.js +34 -0
  8. package/dist/commands/check.js.map +1 -0
  9. package/dist/commands/compile.d.ts +5 -0
  10. package/dist/commands/compile.js +215 -0
  11. package/dist/commands/compile.js.map +1 -0
  12. package/dist/commands/debug.d.ts +5 -0
  13. package/dist/commands/debug.js +59 -0
  14. package/dist/commands/debug.js.map +1 -0
  15. package/dist/commands/diff.d.ts +5 -0
  16. package/dist/commands/diff.js +125 -0
  17. package/dist/commands/diff.js.map +1 -0
  18. package/dist/commands/fmt.d.ts +5 -0
  19. package/dist/commands/fmt.js +49 -0
  20. package/dist/commands/fmt.js.map +1 -0
  21. package/dist/commands/init.d.ts +5 -0
  22. package/dist/commands/init.js +96 -0
  23. package/dist/commands/init.js.map +1 -0
  24. package/dist/commands/ir.d.ts +5 -0
  25. package/dist/commands/ir.js +27 -0
  26. package/dist/commands/ir.js.map +1 -0
  27. package/dist/commands/lex.d.ts +5 -0
  28. package/dist/commands/lex.js +21 -0
  29. package/dist/commands/lex.js.map +1 -0
  30. package/dist/commands/parse.d.ts +5 -0
  31. package/dist/commands/parse.js +30 -0
  32. package/dist/commands/parse.js.map +1 -0
  33. package/dist/commands/test.d.ts +5 -0
  34. package/dist/commands/test.js +61 -0
  35. package/dist/commands/test.js.map +1 -0
  36. package/dist/commands/verify_determinism.d.ts +5 -0
  37. package/dist/commands/verify_determinism.js +64 -0
  38. package/dist/commands/verify_determinism.js.map +1 -0
  39. package/dist/commands/watch.d.ts +5 -0
  40. package/dist/commands/watch.js +50 -0
  41. package/dist/commands/watch.js.map +1 -0
  42. package/dist/emit_auth.d.ts +6 -0
  43. package/dist/emit_auth.js +69 -0
  44. package/dist/emit_auth.js.map +1 -0
  45. package/dist/emit_capability.d.ts +13 -0
  46. package/dist/emit_capability.js +292 -128
  47. package/dist/emit_capability.js.map +1 -1
  48. package/dist/emit_composition.js +37 -3
  49. package/dist/emit_composition.js.map +1 -1
  50. package/dist/emit_database.d.ts +7 -0
  51. package/dist/emit_database.js +74 -0
  52. package/dist/emit_database.js.map +1 -0
  53. package/dist/emit_deploy.js +162 -162
  54. package/dist/emit_events.d.ts +1 -0
  55. package/dist/emit_events.js +342 -275
  56. package/dist/emit_events.js.map +1 -1
  57. package/dist/emit_full.js +135 -95
  58. package/dist/emit_full.js.map +1 -1
  59. package/dist/emit_index.d.ts +6 -0
  60. package/dist/emit_index.js +157 -0
  61. package/dist/emit_index.js.map +1 -0
  62. package/dist/emit_maintenance.js +249 -249
  63. package/dist/emit_models.d.ts +12 -0
  64. package/dist/emit_models.js +171 -0
  65. package/dist/emit_models.js.map +1 -0
  66. package/dist/emit_openapi.d.ts +9 -0
  67. package/dist/emit_openapi.js +308 -0
  68. package/dist/emit_openapi.js.map +1 -0
  69. package/dist/emit_package.d.ts +7 -0
  70. package/dist/emit_package.js +70 -0
  71. package/dist/emit_package.js.map +1 -0
  72. package/dist/emit_router.d.ts +12 -0
  73. package/dist/emit_router.js +390 -0
  74. package/dist/emit_router.js.map +1 -0
  75. package/dist/emit_runtime.d.ts +17 -11
  76. package/dist/emit_runtime.js +29 -686
  77. package/dist/emit_runtime.js.map +1 -1
  78. package/dist/emit_sourcemap.js +66 -66
  79. package/dist/emit_tests.js +37 -0
  80. package/dist/emit_tests.js.map +1 -1
  81. package/dist/emitter.js +34 -5
  82. package/dist/emitter.js.map +1 -1
  83. package/dist/extension_manager.d.ts +2 -2
  84. package/dist/extension_manager.js +6 -3
  85. package/dist/extension_manager.js.map +1 -1
  86. package/dist/lowering.d.ts +5 -14
  87. package/dist/lowering.js +47 -417
  88. package/dist/lowering.js.map +1 -1
  89. package/dist/lowering_channels.d.ts +11 -0
  90. package/dist/lowering_channels.js +102 -0
  91. package/dist/lowering_channels.js.map +1 -0
  92. package/dist/lowering_entities.d.ts +11 -0
  93. package/dist/lowering_entities.js +222 -0
  94. package/dist/lowering_entities.js.map +1 -0
  95. package/dist/lowering_helpers.d.ts +13 -0
  96. package/dist/lowering_helpers.js +76 -0
  97. package/dist/lowering_helpers.js.map +1 -0
  98. package/dist/module_loader.d.ts +2 -2
  99. package/dist/module_loader.js +20 -23
  100. package/dist/module_loader.js.map +1 -1
  101. package/dist/scaffold.d.ts +2 -2
  102. package/dist/scaffold.js +316 -319
  103. package/dist/scaffold.js.map +1 -1
  104. package/dist/typechecker.js +32 -13
  105. package/dist/typechecker.js.map +1 -1
  106. package/dist/verifier.d.ts +5 -0
  107. package/dist/verifier.js +140 -2
  108. package/dist/verifier.js.map +1 -1
  109. package/package.json +62 -52
  110. package/src/algorithm_catalog.ts +345 -345
  111. package/src/ast.ts +334 -334
  112. package/src/cli.ts +98 -624
  113. package/src/commands/check.ts +33 -0
  114. package/src/commands/compile.ts +191 -0
  115. package/src/commands/debug.ts +33 -0
  116. package/src/commands/diff.ts +108 -0
  117. package/src/commands/fmt.ts +22 -0
  118. package/src/commands/init.ts +72 -0
  119. package/src/commands/ir.ts +23 -0
  120. package/src/commands/lex.ts +17 -0
  121. package/src/commands/parse.ts +24 -0
  122. package/src/commands/test.ts +36 -0
  123. package/src/commands/verify_determinism.ts +66 -0
  124. package/src/commands/watch.ts +25 -0
  125. package/src/emit_auth.ts +67 -0
  126. package/src/emit_batch.ts +140 -140
  127. package/src/emit_capability.ts +617 -436
  128. package/src/emit_composition.ts +229 -196
  129. package/src/emit_database.ts +75 -0
  130. package/src/emit_deploy.ts +190 -190
  131. package/src/emit_events.ts +377 -307
  132. package/src/emit_extras.ts +240 -240
  133. package/src/emit_full.ts +351 -309
  134. package/src/emit_index.ts +161 -0
  135. package/src/emit_maintenance.ts +459 -459
  136. package/src/emit_models.ts +176 -0
  137. package/src/emit_openapi.ts +318 -0
  138. package/src/emit_package.ts +69 -0
  139. package/src/emit_router.ts +409 -0
  140. package/src/emit_runtime.ts +17 -728
  141. package/src/emit_sourcemap.ts +140 -140
  142. package/src/emit_tests.ts +246 -205
  143. package/src/emit_websocket.ts +229 -229
  144. package/src/emitter.ts +31 -5
  145. package/src/extension_manager.ts +189 -187
  146. package/src/formatter.ts +297 -297
  147. package/src/index.ts +88 -88
  148. package/src/ir.ts +215 -215
  149. package/src/lexer.ts +630 -630
  150. package/src/lowering.ts +142 -556
  151. package/src/lowering_channels.ts +107 -0
  152. package/src/lowering_entities.ts +248 -0
  153. package/src/lowering_helpers.ts +75 -0
  154. package/src/module_loader.ts +112 -114
  155. package/src/optimizer.ts +196 -196
  156. package/src/parse_decls.ts +409 -409
  157. package/src/parse_decls2.ts +244 -244
  158. package/src/parse_expr.ts +197 -197
  159. package/src/parse_types.ts +54 -54
  160. package/src/parser.ts +1 -1
  161. package/src/parser_base.ts +57 -57
  162. package/src/parser_recovery.ts +153 -153
  163. package/src/scaffold.ts +372 -375
  164. package/src/solver.ts +330 -330
  165. package/src/typechecker.ts +30 -15
  166. package/src/types.ts +122 -122
  167. package/src/verifier.ts +151 -4
@@ -0,0 +1,107 @@
1
+ /**
2
+ * BoneScript Channel / Event / Flow / Store Lowering
3
+ * Converts ChannelDecl, EventDecl, FlowDecl, and StoreDecl AST nodes into IR.
4
+ */
5
+
6
+ import * as AST from "./ast";
7
+ import * as IR from "./ir";
8
+ import { makeId, parseDurationMs, serializeExpr } from "./lowering_helpers";
9
+ import { lowerField as lowerFieldHelper } from "./lowering_entities";
10
+
11
+ // Re-export lowerField so lowering.ts can use a single import
12
+ export { lowerField } from "./lowering_entities";
13
+
14
+ // ─── Store Lowering ───────────────────────────────────────────────────────────
15
+
16
+ export function lowerStore(systemName: string, store: AST.StoreDeclNode): IR.IRModule {
17
+ const entityName = store.name.replace(/Store$/, "") || store.name;
18
+ const model: IR.IRModel = {
19
+ name: entityName,
20
+ fields: store.schema.map(lowerFieldHelper),
21
+ primary_key: "id",
22
+ indexes: [],
23
+ constraints: [],
24
+ };
25
+
26
+ if (!model.fields.find(f => f.name === "id")) {
27
+ model.fields.unshift({
28
+ name: "id", type: "uuid", nullable: false, unique: true, indexed: true, default_value: "gen_random_uuid()",
29
+ });
30
+ }
31
+
32
+ return {
33
+ id: makeId(systemName, "data_store", store.name),
34
+ kind: "data_store",
35
+ name: store.name,
36
+ interfaces: [],
37
+ models: [model],
38
+ events: [],
39
+ state_machines: [],
40
+ relations: [],
41
+ dependencies: [],
42
+ config: {
43
+ engine: store.engine || "postgresql",
44
+ replicas: store.replicas || 1,
45
+ ...(store.retention ? { retention_ms: parseDurationMs(store.retention) || 0 } : {}),
46
+ ...(store.partition ? { partition_key: store.partition } : {}),
47
+ },
48
+ };
49
+ }
50
+
51
+ // ─── Channel Lowering ─────────────────────────────────────────────────────────
52
+
53
+ export function lowerChannel(systemName: string, channel: AST.ChannelDeclNode): IR.IRModule {
54
+ return {
55
+ id: makeId(systemName, "realtime_service", channel.name),
56
+ kind: "realtime_service",
57
+ name: channel.name,
58
+ interfaces: [{
59
+ name: `I${channel.name}Channel`,
60
+ methods: [
61
+ { name: "connect", input: [], output: "connection", preconditions: [], effects: [], emissions: [], idempotent: false, authenticated: true, timeout_ms: 5000, retry: null, pipeline: null, algorithm: null, sync: null },
62
+ { name: "subscribe", input: [{ name: "topic", type: "string", nullable: false, unique: false, indexed: false, default_value: null }], output: "subscription", preconditions: [], effects: [], emissions: [], idempotent: true, authenticated: true, timeout_ms: 5000, retry: null, pipeline: null, algorithm: null, sync: null },
63
+ { name: "publish", input: [{ name: "message", type: "json", nullable: false, unique: false, indexed: false, default_value: null }], output: "void", preconditions: [], effects: [], emissions: [], idempotent: false, authenticated: true, timeout_ms: 5000, retry: null, pipeline: null, algorithm: null, sync: null },
64
+ ],
65
+ }],
66
+ models: [],
67
+ events: [],
68
+ state_machines: [],
69
+ relations: [],
70
+ dependencies: [],
71
+ config: {
72
+ transport: channel.transport || "websocket",
73
+ ordering: channel.ordering || "fifo",
74
+ persistence: channel.persistence || "none",
75
+ max_size: channel.maxSize || 10000,
76
+ },
77
+ };
78
+ }
79
+
80
+ // ─── Event Lowering ───────────────────────────────────────────────────────────
81
+
82
+ export function lowerEvent(systemName: string, ev: AST.EventDeclNode, source: string): IR.IREvent {
83
+ return {
84
+ id: makeId(systemName, "event", ev.name),
85
+ name: ev.name,
86
+ payload: ev.payload.map(lowerFieldHelper),
87
+ source,
88
+ delivery: (ev.delivery as IR.IRDeliveryMode) || "at_least_once",
89
+ ordering: "fifo",
90
+ ttl_ms: parseDurationMs(ev.ttl),
91
+ };
92
+ }
93
+
94
+ // ─── Flow Lowering ────────────────────────────────────────────────────────────
95
+
96
+ export function lowerFlow(_systemName: string, flow: AST.FlowDeclNode): IR.IRFlow {
97
+ return {
98
+ name: flow.name,
99
+ steps: flow.steps.map(s => ({
100
+ name: s.name,
101
+ action: `${s.action.name}(${s.action.args.map(serializeExpr).join(", ")})`,
102
+ compensation: s.compensate
103
+ ? `${s.compensate.name}(${s.compensate.args.map(serializeExpr).join(", ")})`
104
+ : null,
105
+ })),
106
+ };
107
+ }
@@ -0,0 +1,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 { 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
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * BoneScript Lowering Helpers
3
+ * Shared utilities used across all lowering phases:
4
+ * - Deterministic ID generation
5
+ * - Duration string parsing
6
+ * - AST type/expression serialization
7
+ */
8
+
9
+ import { createHash } from "crypto";
10
+ import * as AST from "./ast";
11
+
12
+ // ─── Deterministic ID Generation ─────────────────────────────────────────────
13
+
14
+ export function makeId(systemName: string, kind: string, name: string): string {
15
+ return createHash("sha256")
16
+ .update(`${systemName}.${kind}.${name}`)
17
+ .digest("hex")
18
+ .slice(0, 16);
19
+ }
20
+
21
+ // ─── Duration Parsing ─────────────────────────────────────────────────────────
22
+
23
+ export function parseDurationMs(dur: string | null): number | null {
24
+ if (!dur) return null;
25
+ const match = dur.match(/^(\d+)(ms|s|m|h|d)$/);
26
+ if (!match) return null;
27
+ const value = parseInt(match[1], 10);
28
+ switch (match[2]) {
29
+ case "ms": return value;
30
+ case "s": return value * 1_000;
31
+ case "m": return value * 60_000;
32
+ case "h": return value * 3_600_000;
33
+ case "d": return value * 86_400_000;
34
+ default: return null;
35
+ }
36
+ }
37
+
38
+ // ─── Type Expression Serialization ───────────────────────────────────────────
39
+
40
+ export function serializeType(t: AST.TypeExprNode): string {
41
+ switch (t.kind) {
42
+ case "PrimitiveType": return t.name;
43
+ case "GenericType": return `${t.name}<${t.typeArgs.map(serializeType).join(", ")}>`;
44
+ case "EntityRefType": return t.name;
45
+ case "TupleType": return `(${t.elements.map(serializeType).join(", ")})`;
46
+ case "UnionType": return t.members.map(serializeType).join(" | ");
47
+ }
48
+ }
49
+
50
+ // ─── Expression Serialization ─────────────────────────────────────────────────
51
+
52
+ export function serializeExpr(e: AST.ExprNode): string {
53
+ switch (e.kind) {
54
+ case "Literal":
55
+ if (e.type === "string") return `"${e.value}"`;
56
+ if (e.type === "list") return `[${(e.value as AST.ExprNode[]).map(serializeExpr).join(", ")}]`;
57
+ return String(e.value);
58
+ case "FieldRef":
59
+ return e.path.join(".");
60
+ case "BinaryExpr":
61
+ return `(${serializeExpr(e.left)} ${e.op} ${serializeExpr(e.right)})`;
62
+ case "UnaryExpr":
63
+ return `(${e.op} ${serializeExpr(e.operand)})`;
64
+ case "CallExpr":
65
+ return `${e.name}(${e.args.map(serializeExpr).join(", ")})`;
66
+ case "TernaryExpr":
67
+ return `(${serializeExpr(e.condition)} ? ${serializeExpr(e.consequent)} : ${serializeExpr(e.alternate)})`;
68
+ }
69
+ }
70
+
71
+ // ─── Shared snake_case helper ─────────────────────────────────────────────────
72
+
73
+ export function toSnakeCase(s: string): string {
74
+ return s.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase();
75
+ }
@@ -1,114 +1,112 @@
1
- /**
2
- * BoneScript Module Loader — Resolves import declarations across multiple .bone files.
3
- *
4
- * Behavior:
5
- * - Tracks loaded files to avoid cycles
6
- * - Resolves relative paths from importing file
7
- * - Merges imported declarations into a single AST
8
- */
9
-
10
- import * as fs from "fs";
11
- import * as path from "path";
12
- import { Lexer } from "./lexer";
13
- import { Parser } from "./parser";
14
- import { RecoveringParser } from "./parser_recovery";
15
- import { ParseError } from "./parser_base";
16
- import * as AST from "./ast";
17
-
18
- export interface LoadResult {
19
- ast: AST.ProgramNode | null;
20
- errors: { file: string; error: ParseError }[];
21
- loadedFiles: string[];
22
- }
23
-
24
- export class ModuleLoader {
25
- private loaded = new Map<string, AST.ProgramNode>();
26
- private inProgress = new Set<string>();
27
- private errors: { file: string; error: ParseError }[] = [];
28
-
29
- load(entryFile: string): LoadResult {
30
- const resolved = path.resolve(entryFile);
31
- const ast = this.loadFile(resolved);
32
-
33
- return {
34
- ast,
35
- errors: this.errors,
36
- loadedFiles: Array.from(this.loaded.keys()),
37
- };
38
- }
39
-
40
- private loadFile(filePath: string): AST.ProgramNode | null {
41
- if (this.loaded.has(filePath)) return this.loaded.get(filePath)!;
42
- if (this.inProgress.has(filePath)) {
43
- this.errors.push({
44
- file: filePath,
45
- error: new ParseError(`Circular import detected: ${filePath}`, { line: 1, column: 1, offset: 0 }),
46
- });
47
- return null;
48
- }
49
-
50
- if (!fs.existsSync(filePath)) {
51
- this.errors.push({
52
- file: filePath,
53
- error: new ParseError(`File not found: ${filePath}`, { line: 1, column: 1, offset: 0 }),
54
- });
55
- return null;
56
- }
57
-
58
- this.inProgress.add(filePath);
59
-
60
- const source = fs.readFileSync(filePath, "utf-8");
61
- const tokens = new Lexer(source).tokenize();
62
- const result = new RecoveringParser(tokens).parse();
63
-
64
- for (const err of result.errors) {
65
- this.errors.push({ file: filePath, error: err });
66
- }
67
-
68
- if (!result.ast) {
69
- this.inProgress.delete(filePath);
70
- return null;
71
- }
72
-
73
- // Resolve imports recursively
74
- const importedSystems: AST.SystemDeclNode[] = [];
75
- for (const sys of result.ast.systems) {
76
- const imports = sys.declarations.filter((d): d is AST.ImportDeclNode => d.kind === "ImportDecl");
77
- for (const imp of imports) {
78
- const importPath = path.resolve(path.dirname(filePath), imp.from);
79
- const importedAst = this.loadFile(importPath);
80
- if (importedAst) {
81
- importedSystems.push(...importedAst.systems);
82
- }
83
- }
84
- }
85
-
86
- // Merge imported systems' declarations into current systems
87
- if (importedSystems.length > 0) {
88
- const mergedSystems = result.ast.systems.map(sys => {
89
- const importedDecls: AST.DeclarationNode[] = [];
90
- for (const imported of importedSystems) {
91
- // Add imported entities, events, etc. (skip imports themselves)
92
- for (const d of imported.declarations) {
93
- if (d.kind !== "ImportDecl") importedDecls.push(d);
94
- }
95
- }
96
- return {
97
- ...sys,
98
- declarations: [...sys.declarations.filter(d => d.kind !== "ImportDecl"), ...importedDecls],
99
- };
100
- });
101
- result.ast.systems = mergedSystems;
102
- } else {
103
- // Remove import declarations from final AST
104
- result.ast.systems = result.ast.systems.map(sys => ({
105
- ...sys,
106
- declarations: sys.declarations.filter(d => d.kind !== "ImportDecl"),
107
- }));
108
- }
109
-
110
- this.loaded.set(filePath, result.ast);
111
- this.inProgress.delete(filePath);
112
- return result.ast;
113
- }
114
- }
1
+ /**
2
+ * BoneScript Module Loader Resolves import declarations across multiple .bone files.
3
+ *
4
+ * Behavior:
5
+ * - Tracks loaded files to avoid cycles
6
+ * - Resolves relative paths from importing file
7
+ * - Merges imported declarations into a single AST
8
+ */
9
+
10
+ import * as fs from "fs";
11
+ import * as path from "path";
12
+ import { Lexer } from "./lexer";
13
+ import { RecoveringParser } from "./parser_recovery";
14
+ import { ParseError } from "./parser_base";
15
+ import * as AST from "./ast";
16
+
17
+ export interface LoadResult {
18
+ ast: AST.ProgramNode | null;
19
+ errors: { file: string; error: ParseError }[];
20
+ loadedFiles: string[];
21
+ }
22
+
23
+ export class ModuleLoader {
24
+ private loaded = new Map<string, AST.ProgramNode>();
25
+ private inProgress = new Set<string>();
26
+ private errors: { file: string; error: ParseError }[] = [];
27
+
28
+ async load(entryFile: string): Promise<LoadResult> {
29
+ const resolved = path.resolve(entryFile);
30
+ const ast = await this.loadFile(resolved);
31
+ return {
32
+ ast,
33
+ errors: this.errors,
34
+ loadedFiles: Array.from(this.loaded.keys()),
35
+ };
36
+ }
37
+
38
+ private async loadFile(filePath: string): Promise<AST.ProgramNode | null> {
39
+ if (this.loaded.has(filePath)) return this.loaded.get(filePath)!;
40
+ if (this.inProgress.has(filePath)) {
41
+ this.errors.push({
42
+ file: filePath,
43
+ error: new ParseError(`Circular import detected: ${filePath}`, { line: 1, column: 1, offset: 0 }),
44
+ });
45
+ return null;
46
+ }
47
+
48
+ // Check existence without blocking the event loop
49
+ try {
50
+ await fs.promises.access(filePath);
51
+ } catch {
52
+ this.errors.push({
53
+ file: filePath,
54
+ error: new ParseError(`File not found: ${filePath}`, { line: 1, column: 1, offset: 0 }),
55
+ });
56
+ return null;
57
+ }
58
+
59
+ this.inProgress.add(filePath);
60
+
61
+ const source = await fs.promises.readFile(filePath, "utf-8");
62
+ const tokens = new Lexer(source).tokenize();
63
+ const result = new RecoveringParser(tokens).parse();
64
+
65
+ for (const err of result.errors) {
66
+ this.errors.push({ file: filePath, error: err });
67
+ }
68
+
69
+ if (!result.ast) {
70
+ this.inProgress.delete(filePath);
71
+ return null;
72
+ }
73
+
74
+ // Resolve imports recursively (in parallel where possible)
75
+ const importedSystems: AST.SystemDeclNode[] = [];
76
+ for (const sys of result.ast.systems) {
77
+ const imports = sys.declarations.filter((d): d is AST.ImportDeclNode => d.kind === "ImportDecl");
78
+ // Load all imports for this system in parallel
79
+ const importedAsts = await Promise.all(
80
+ imports.map(imp => {
81
+ const importPath = path.resolve(path.dirname(filePath), imp.from);
82
+ return this.loadFile(importPath);
83
+ })
84
+ );
85
+ for (const importedAst of importedAsts) {
86
+ if (importedAst) importedSystems.push(...importedAst.systems);
87
+ }
88
+ }
89
+
90
+ // Merge imported declarations into current systems
91
+ if (importedSystems.length > 0) {
92
+ result.ast.systems = result.ast.systems.map(sys => {
93
+ const importedDecls: AST.DeclarationNode[] = importedSystems.flatMap(imported =>
94
+ imported.declarations.filter(d => d.kind !== "ImportDecl")
95
+ );
96
+ return {
97
+ ...sys,
98
+ declarations: [...sys.declarations.filter(d => d.kind !== "ImportDecl"), ...importedDecls],
99
+ };
100
+ });
101
+ } else {
102
+ result.ast.systems = result.ast.systems.map(sys => ({
103
+ ...sys,
104
+ declarations: sys.declarations.filter(d => d.kind !== "ImportDecl"),
105
+ }));
106
+ }
107
+
108
+ this.loaded.set(filePath, result.ast);
109
+ this.inProgress.delete(filePath);
110
+ return result.ast;
111
+ }
112
+ }