bonescript-compiler 0.2.0 → 0.3.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 (146) 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 +183 -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 +69 -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 +235 -125
  47. package/dist/emit_capability.js.map +1 -1
  48. package/dist/emit_database.d.ts +7 -0
  49. package/dist/emit_database.js +74 -0
  50. package/dist/emit_database.js.map +1 -0
  51. package/dist/emit_deploy.js +162 -162
  52. package/dist/emit_events.js +274 -274
  53. package/dist/emit_full.js +102 -95
  54. package/dist/emit_full.js.map +1 -1
  55. package/dist/emit_index.d.ts +6 -0
  56. package/dist/emit_index.js +157 -0
  57. package/dist/emit_index.js.map +1 -0
  58. package/dist/emit_maintenance.js +249 -249
  59. package/dist/emit_package.d.ts +7 -0
  60. package/dist/emit_package.js +70 -0
  61. package/dist/emit_package.js.map +1 -0
  62. package/dist/emit_router.d.ts +12 -0
  63. package/dist/emit_router.js +375 -0
  64. package/dist/emit_router.js.map +1 -0
  65. package/dist/emit_runtime.d.ts +17 -11
  66. package/dist/emit_runtime.js +29 -686
  67. package/dist/emit_runtime.js.map +1 -1
  68. package/dist/emit_sourcemap.js +66 -66
  69. package/dist/extension_manager.d.ts +2 -2
  70. package/dist/extension_manager.js +6 -3
  71. package/dist/extension_manager.js.map +1 -1
  72. package/dist/lowering.d.ts +5 -14
  73. package/dist/lowering.js +32 -417
  74. package/dist/lowering.js.map +1 -1
  75. package/dist/lowering_channels.d.ts +11 -0
  76. package/dist/lowering_channels.js +102 -0
  77. package/dist/lowering_channels.js.map +1 -0
  78. package/dist/lowering_entities.d.ts +11 -0
  79. package/dist/lowering_entities.js +222 -0
  80. package/dist/lowering_entities.js.map +1 -0
  81. package/dist/lowering_helpers.d.ts +13 -0
  82. package/dist/lowering_helpers.js +76 -0
  83. package/dist/lowering_helpers.js.map +1 -0
  84. package/dist/module_loader.d.ts +2 -2
  85. package/dist/module_loader.js +20 -23
  86. package/dist/module_loader.js.map +1 -1
  87. package/dist/scaffold.d.ts +2 -2
  88. package/dist/scaffold.js +316 -319
  89. package/dist/scaffold.js.map +1 -1
  90. package/package.json +62 -52
  91. package/src/algorithm_catalog.ts +345 -345
  92. package/src/ast.ts +334 -334
  93. package/src/cli.ts +98 -624
  94. package/src/commands/check.ts +33 -0
  95. package/src/commands/compile.ts +160 -0
  96. package/src/commands/debug.ts +33 -0
  97. package/src/commands/diff.ts +108 -0
  98. package/src/commands/fmt.ts +22 -0
  99. package/src/commands/init.ts +46 -0
  100. package/src/commands/ir.ts +23 -0
  101. package/src/commands/lex.ts +17 -0
  102. package/src/commands/parse.ts +24 -0
  103. package/src/commands/test.ts +36 -0
  104. package/src/commands/verify_determinism.ts +66 -0
  105. package/src/commands/watch.ts +25 -0
  106. package/src/emit_auth.ts +67 -0
  107. package/src/emit_batch.ts +140 -140
  108. package/src/emit_capability.ts +562 -436
  109. package/src/emit_composition.ts +196 -196
  110. package/src/emit_database.ts +75 -0
  111. package/src/emit_deploy.ts +190 -190
  112. package/src/emit_events.ts +307 -307
  113. package/src/emit_extras.ts +240 -240
  114. package/src/emit_full.ts +316 -309
  115. package/src/emit_index.ts +161 -0
  116. package/src/emit_maintenance.ts +459 -459
  117. package/src/emit_package.ts +69 -0
  118. package/src/emit_router.ts +395 -0
  119. package/src/emit_runtime.ts +17 -728
  120. package/src/emit_sourcemap.ts +140 -140
  121. package/src/emit_tests.ts +205 -205
  122. package/src/emit_websocket.ts +229 -229
  123. package/src/emitter.ts +566 -566
  124. package/src/extension_manager.ts +189 -187
  125. package/src/formatter.ts +297 -297
  126. package/src/index.ts +88 -88
  127. package/src/ir.ts +215 -215
  128. package/src/lexer.ts +630 -630
  129. package/src/lowering.ts +124 -556
  130. package/src/lowering_channels.ts +107 -0
  131. package/src/lowering_entities.ts +248 -0
  132. package/src/lowering_helpers.ts +75 -0
  133. package/src/module_loader.ts +112 -114
  134. package/src/optimizer.ts +196 -196
  135. package/src/parse_decls.ts +409 -409
  136. package/src/parse_decls2.ts +244 -244
  137. package/src/parse_expr.ts +197 -197
  138. package/src/parse_types.ts +54 -54
  139. package/src/parser.ts +1 -1
  140. package/src/parser_base.ts +57 -57
  141. package/src/parser_recovery.ts +153 -153
  142. package/src/scaffold.ts +372 -375
  143. package/src/solver.ts +330 -330
  144. package/src/typechecker.ts +591 -591
  145. package/src/types.ts +122 -122
  146. package/src/verifier.ts +348 -348
@@ -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): IR.IREvent {
83
+ return {
84
+ id: makeId(systemName, "event", ev.name),
85
+ name: ev.name,
86
+ payload: ev.payload.map(lowerFieldHelper),
87
+ source: "unknown",
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
+ }