bonescript-compiler 0.5.2 → 0.5.3

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 (57) hide show
  1. package/dist/commands/diff.js +3 -5
  2. package/dist/commands/diff.js.map +1 -1
  3. package/dist/emit_capability.js +6 -8
  4. package/dist/emit_capability.js.map +1 -1
  5. package/dist/emit_database.js +2 -4
  6. package/dist/emit_database.js.map +1 -1
  7. package/dist/emit_deploy.js +6 -8
  8. package/dist/emit_deploy.js.map +1 -1
  9. package/dist/emit_events.js +2 -19
  10. package/dist/emit_events.js.map +1 -1
  11. package/dist/emit_extras.js +3 -5
  12. package/dist/emit_extras.js.map +1 -1
  13. package/dist/emit_full.js +9 -11
  14. package/dist/emit_full.js.map +1 -1
  15. package/dist/emit_openapi.js +2 -4
  16. package/dist/emit_openapi.js.map +1 -1
  17. package/dist/emit_package.js +2 -4
  18. package/dist/emit_package.js.map +1 -1
  19. package/dist/emit_router.js +3 -4
  20. package/dist/emit_router.js.map +1 -1
  21. package/dist/emit_tests.js +16 -18
  22. package/dist/emit_tests.js.map +1 -1
  23. package/dist/emit_websocket.js +0 -3
  24. package/dist/emit_websocket.js.map +1 -1
  25. package/dist/emitter.js +27 -48
  26. package/dist/emitter.js.map +1 -1
  27. package/dist/extension_manager.js +3 -17
  28. package/dist/extension_manager.js.map +1 -1
  29. package/dist/optimizer.js +6 -3
  30. package/dist/optimizer.js.map +1 -1
  31. package/dist/solver.js +1 -1
  32. package/dist/solver.js.map +1 -1
  33. package/dist/typechecker.d.ts +2 -0
  34. package/dist/typechecker.js +35 -3
  35. package/dist/typechecker.js.map +1 -1
  36. package/dist/verifier.js +2 -3
  37. package/dist/verifier.js.map +1 -1
  38. package/package.json +3 -3
  39. package/src/commands/compile.ts +191 -191
  40. package/src/commands/diff.ts +1 -4
  41. package/src/emit_capability.ts +1 -5
  42. package/src/emit_database.ts +1 -4
  43. package/src/emit_deploy.ts +1 -4
  44. package/src/emit_events.ts +1 -16
  45. package/src/emit_extras.ts +1 -4
  46. package/src/emit_full.ts +1 -4
  47. package/src/emit_openapi.ts +1 -4
  48. package/src/emit_package.ts +1 -4
  49. package/src/emit_router.ts +1 -2
  50. package/src/emit_tests.ts +1 -4
  51. package/src/emit_websocket.ts +1 -4
  52. package/src/emitter.ts +12 -28
  53. package/src/extension_manager.ts +1 -13
  54. package/src/optimizer.ts +6 -3
  55. package/src/solver.ts +1 -1
  56. package/src/typechecker.ts +47 -4
  57. package/src/verifier.ts +4 -5
@@ -18,8 +18,8 @@
18
18
  */
19
19
 
20
20
  import * as IR from "./ir";
21
+ import { toSnakeCase } from "./lowering_helpers";
21
22
 
22
- // ─── Expression Parser ────────────────────────────────────────────────────────
23
23
 
24
24
  type ExprKind =
25
25
  | { kind: "literal"; value: string; raw: string }
@@ -88,10 +88,6 @@ interface EntityFetch {
88
88
  idField: string;
89
89
  }
90
90
 
91
- function toSnakeCase(s: string): string {
92
- return s.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase();
93
- }
94
-
95
91
  function getEntityFetches(method: IR.IRMethod, mod: IR.IRModule, system: IR.IRSystem): EntityFetch[] {
96
92
  const fetches: EntityFetch[] = [];
97
93
  const seen = new Set<string>();
@@ -4,10 +4,7 @@
4
4
  */
5
5
 
6
6
  import * as IR from "./ir";
7
-
8
- function toSnakeCase(s: string): string {
9
- return s.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase();
10
- }
7
+ import { toSnakeCase } from "./lowering_helpers";
11
8
 
12
9
  export function emitDbClient(system: IR.IRSystem): string {
13
10
  const name = toSnakeCase(system.name);
@@ -4,10 +4,7 @@
4
4
  */
5
5
 
6
6
  import * as IR from "./ir";
7
-
8
- function toSnakeCase(s: string): string {
9
- return s.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase();
10
- }
7
+ import { toSnakeCase } from "./lowering_helpers";
11
8
 
12
9
  export function emitDockerfile(system: IR.IRSystem): string {
13
10
  return `# Generated by BoneScript compiler.
@@ -11,6 +11,7 @@
11
11
  */
12
12
 
13
13
  import * as IR from "./ir";
14
+ import { toTsType } from "./emit_router";
14
15
 
15
16
  // ─── Outbox SQL Schema ────────────────────────────────────────────────────────
16
17
 
@@ -311,22 +312,6 @@ export const eventBus = {
311
312
  // spec/09_CODEGEN.md §5.4. These wrap eventBus.publish with a typed payload
312
313
  // interface so callers get compile-time safety instead of raw Record<string,unknown>.
313
314
 
314
- const TS_TYPE_MAP: Record<string, string> = {
315
- string: "string", uint: "number", int: "number", float: "number",
316
- bool: "boolean", timestamp: "Date", uuid: "string", bytes: "Buffer", json: "unknown",
317
- };
318
-
319
- function toTsType(irType: string): string {
320
- if (TS_TYPE_MAP[irType]) return TS_TYPE_MAP[irType];
321
- const listMatch = irType.match(/^list<(.+)>$/);
322
- if (listMatch) return `${toTsType(listMatch[1])}[]`;
323
- const setMatch = irType.match(/^set<(.+)>$/);
324
- if (setMatch) return `${toTsType(setMatch[1])}[]`;
325
- const optMatch = irType.match(/^optional<(.+)>$/);
326
- if (optMatch) return `${toTsType(optMatch[1])} | null`;
327
- return irType;
328
- }
329
-
330
315
  export function emitTypedEventPublishers(system: IR.IRSystem): string {
331
316
  if (system.events.length === 0) return "";
332
317
 
@@ -8,10 +8,7 @@
8
8
 
9
9
  import * as IR from "./ir";
10
10
  import * as AST from "./ast";
11
-
12
- function toSnakeCase(s: string): string {
13
- return s.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase();
14
- }
11
+ import { toSnakeCase } from "./lowering_helpers";
15
12
 
16
13
  // ─── Derived Field Emission ──────────────────────────────────────────────────
17
14
  // Derived fields become PostgreSQL generated columns (when expression supports it)
package/src/emit_full.ts CHANGED
@@ -34,10 +34,7 @@ import { emitTestSuite } from "./emit_tests";
34
34
  import { emitDockerfile, emitDockerignore, emitK8sDeployment, emitGithubActions } from "./emit_deploy";
35
35
  import { emitModelFile, emitModelsIndex } from "./emit_models";
36
36
  import { emitOpenApiSchema } from "./emit_openapi";
37
-
38
- function toSnakeCase(s: string): string {
39
- return s.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase();
40
- }
37
+ import { toSnakeCase } from "./lowering_helpers";
41
38
 
42
39
  /** Resolve the auth method for the system from the resolution map or module configs. */
43
40
  function resolveSystemAuthMethod(system: IR.IRSystem): "jwt" | "oauth2" | "apikey" {
@@ -7,6 +7,7 @@
7
7
  */
8
8
 
9
9
  import * as IR from "./ir";
10
+ import { toSnakeCase } from "./lowering_helpers";
10
11
 
11
12
  // ─── Type mapping ─────────────────────────────────────────────────────────────
12
13
 
@@ -53,10 +54,6 @@ function modelToSchema(model: IR.IRModel): Record<string, unknown> {
53
54
  };
54
55
  }
55
56
 
56
- function toSnakeCase(s: string): string {
57
- return s.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase();
58
- }
59
-
60
57
  // ─── Main emitter ─────────────────────────────────────────────────────────────
61
58
 
62
59
  export function emitOpenApiSchema(system: IR.IRSystem): string {
@@ -4,10 +4,7 @@
4
4
  */
5
5
 
6
6
  import * as IR from "./ir";
7
-
8
- function toSnakeCase(s: string): string {
9
- return s.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase();
10
- }
7
+ import { toSnakeCase } from "./lowering_helpers";
11
8
 
12
9
  export function emitPackageJson(system: IR.IRSystem): string {
13
10
  const pkg = {
@@ -6,6 +6,7 @@
6
6
 
7
7
  import * as IR from "./ir";
8
8
  import { emitCapabilityBody } from "./emit_capability";
9
+ import { emitPipelineBody, emitAlgorithmBody } from "./emit_composition";
9
10
 
10
11
  // ─── Shared helpers ───────────────────────────────────────────────────────────
11
12
 
@@ -361,10 +362,8 @@ export function emitCapabilityEndpoint(
361
362
  }
362
363
 
363
364
  if (method.pipeline) {
364
- const { emitPipelineBody } = require("./emit_composition");
365
365
  lines.push(emitPipelineBody(method, " "));
366
366
  } else if (method.algorithm) {
367
- const { emitAlgorithmBody } = require("./emit_composition");
368
367
  lines.push(emitAlgorithmBody(method, " "));
369
368
  } else {
370
369
  lines.push(emitCapabilityBody(method, mod, system, " "));
package/src/emit_tests.ts CHANGED
@@ -13,10 +13,7 @@
13
13
  */
14
14
 
15
15
  import * as IR from "./ir";
16
-
17
- function toSnakeCase(s: string): string {
18
- return s.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase();
19
- }
16
+ import { toSnakeCase } from "./lowering_helpers";
20
17
 
21
18
  export function emitTestSuite(system: IR.IRSystem): string {
22
19
  const lines: string[] = [];
@@ -4,10 +4,7 @@
4
4
  */
5
5
 
6
6
  import * as IR from "./ir";
7
-
8
- function toSnakeCase(s: string): string {
9
- return s.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase();
10
- }
7
+ import { toSnakeCase } from "./lowering_helpers";
11
8
 
12
9
  function toCamelCase(s: string): string {
13
10
  return s.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
package/src/emitter.ts CHANGED
@@ -1,4 +1,4 @@
1
- /**
1
+ /**
2
2
  * BoneScript Code Emitter — Stage 6 of the compilation pipeline.
3
3
  * Implements spec/09_CODEGEN.md.
4
4
  *
@@ -7,6 +7,10 @@
7
7
  */
8
8
 
9
9
  import * as IR from "./ir";
10
+ import { emitCapabilityBody } from "./emit_capability";
11
+ import { emitPipelineBody, emitAlgorithmBody } from "./emit_composition";
12
+ import { toSnakeCase } from "./lowering_helpers";
13
+ import { toTsType } from "./emit_router";
10
14
 
11
15
  export interface EmittedFile {
12
16
  path: string;
@@ -41,19 +45,6 @@ const SQL_TYPE_MAP: Record<string, string> = {
41
45
  json: "JSONB",
42
46
  };
43
47
 
44
- function toTsType(irType: string): string {
45
- if (TS_TYPE_MAP[irType]) return TS_TYPE_MAP[irType];
46
- const listMatch = irType.match(/^list<(.+)>$/);
47
- if (listMatch) return `${toTsType(listMatch[1])}[]`;
48
- const setMatch = irType.match(/^set<(.+)>$/);
49
- if (setMatch) return `Set<${toTsType(setMatch[1])}>`;
50
- const mapMatch = irType.match(/^map<(.+),\s*(.+)>$/);
51
- if (mapMatch) return `Map<${toTsType(mapMatch[1])}, ${toTsType(mapMatch[2])}>`;
52
- const optMatch = irType.match(/^optional<(.+)>$/);
53
- if (optMatch) return `${toTsType(optMatch[1])} | null`;
54
- // Entity reference or unknown
55
- return irType;
56
- }
57
48
 
58
49
  function toSqlType(irType: string): string {
59
50
  if (SQL_TYPE_MAP[irType]) return SQL_TYPE_MAP[irType];
@@ -68,9 +59,6 @@ function sqlCheckConstraint(irType: string): string {
68
59
  return "";
69
60
  }
70
61
 
71
- function toSnakeCase(s: string): string {
72
- return s.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase();
73
- }
74
62
 
75
63
  // ─── Emitter ─────────────────────────────────────────────────────────────────
76
64
 
@@ -167,12 +155,6 @@ export class Emitter {
167
155
  // Add cardinality CHECK constraints from relations
168
156
  // has_one: enforce at most 1 child row via a partial unique index (emitted below)
169
157
  // has_many with explicit max: enforce via CHECK on count (done via trigger — see below)
170
- for (const rel of (mod as any).relations_with_cardinality || []) {
171
- if (rel.cardinality && rel.cardinality.max !== "*" && typeof rel.cardinality.max === "number") {
172
- // Will be enforced via trigger — placeholder comment
173
- fieldLines.push(` -- cardinality: ${rel.name} max ${rel.cardinality.max} (enforced by trigger)`);
174
- }
175
- }
176
158
 
177
159
  lines.push(fieldLines.join(",\n"));
178
160
  lines.push(`);`);
@@ -462,8 +444,6 @@ export class Emitter {
462
444
 
463
445
  // Real implementation — delegate to emitCapabilityBody for capabilities,
464
446
  // or generate CRUD SQL for standard methods
465
- const { emitCapabilityBody } = require("./emit_capability");
466
- const { emitPipelineBody, emitAlgorithmBody } = require("./emit_composition");
467
447
 
468
448
  if (method.pipeline) {
469
449
  lines.push(emitPipelineBody(method, " "));
@@ -473,10 +453,14 @@ export class Emitter {
473
453
  // Capability with effects/preconditions — use the full capability body emitter
474
454
  try {
475
455
  lines.push(emitCapabilityBody(method, mod, system, " "));
476
- } catch {
477
- // Fallback: emit a descriptive stub if body generation fails
456
+ } catch (err: unknown) {
457
+ // Body generation failed — emit a visible compile-time warning in the generated
458
+ // file so the developer knows this method needs attention, rather than silently
459
+ // producing a broken stub.
460
+ const msg = err instanceof Error ? err.message : String(err);
461
+ lines.push(` // ⚠ COMPILER WARNING: emitCapabilityBody failed for '${method.name}': ${msg.replace(/`/g, "'")}`);
478
462
  lines.push(` // Effects: ${method.effects.map((e: any) => e.target + " " + e.op + " " + e.value).join("; ")}`);
479
- lines.push(` return { ok: false, error: { code: "NOT_IMPLEMENTED", message: "${method.name} not yet implemented" } };`);
463
+ lines.push(` return { ok: false, error: { code: "CODEGEN_FAILED", message: "Body generation failed for '${method.name}': " + ${JSON.stringify(msg)} } };`);
480
464
  }
481
465
  } else {
482
466
  // CRUD or simple method — emit a typed not-implemented stub
@@ -19,6 +19,7 @@
19
19
  import * as fs from "fs";
20
20
  import * as path from "path";
21
21
  import * as AST from "./ast";
22
+ import { toTsType } from "./emit_router";
22
23
 
23
24
  // ─── Sentinel Helpers ────────────────────────────────────────────────────────
24
25
 
@@ -36,19 +37,6 @@ export function isStubImplementation(code: string): boolean {
36
37
 
37
38
  // ─── Stub Generator ──────────────────────────────────────────────────────────
38
39
 
39
- function toTsType(irType: string): string {
40
- const map: Record<string, string> = {
41
- string: "string", uint: "number", int: "number", float: "number",
42
- bool: "boolean", timestamp: "Date", uuid: "string", bytes: "Buffer", json: "unknown",
43
- };
44
- if (map[irType]) return map[irType];
45
- const m = irType.match(/^(list|set)<(.+)>$/);
46
- if (m) return `${toTsType(m[2])}[]`;
47
- const om = irType.match(/^optional<(.+)>$/);
48
- if (om) return `${toTsType(om[1])} | null`;
49
- return irType;
50
- }
51
-
52
40
  function serializeType(t: AST.TypeExprNode): string {
53
41
  switch (t.kind) {
54
42
  case "PrimitiveType": return t.name;
package/src/optimizer.ts CHANGED
@@ -50,14 +50,17 @@ export function optimize(system: IR.IRSystem): OptimizationResult {
50
50
  function deadModuleElimination(s: IR.IRSystem, log: OptimizationLog[]): IR.IRSystem {
51
51
  const reachable = new Set<string>();
52
52
 
53
- // Seed: always-reachable kinds
53
+ // Seed: entry-point kinds that are always reachable by definition.
54
+ // worker_service, event_bus, and cache are infrastructure modules that are
55
+ // only reachable if something depends on them — they are NOT seeded here so
56
+ // that truly orphaned infrastructure modules can be eliminated.
54
57
  for (const m of s.modules) {
55
- if (["gateway", "frontend", "auth_service", "api_service", "realtime_service", "data_store", "event_bus", "cache"].includes(m.kind)) {
58
+ if (["gateway", "frontend", "auth_service", "api_service", "realtime_service", "data_store"].includes(m.kind)) {
56
59
  reachable.add(m.id);
57
60
  }
58
61
  }
59
62
 
60
- // Propagate reachability
63
+ // Propagate reachability through the dependency graph
61
64
  let changed = true;
62
65
  while (changed) {
63
66
  changed = false;
package/src/solver.ts CHANGED
@@ -59,7 +59,7 @@ const DOMAIN_DEFAULTS: Record<string, DomainDefaults> = {
59
59
  },
60
60
  iot_system: {
61
61
  auth: "apikey",
62
- engine: "dynamodb",
62
+ engine: "postgresql", // dynamodb is not yet supported by the emitter (T014); use postgresql
63
63
  session_engine: "redis",
64
64
  sync: "eventual",
65
65
  channel_transport: "grpc_stream",
@@ -1,4 +1,4 @@
1
- /**
1
+ /**
2
2
  * BoneScript Type Checker — Stage 3 of the compilation pipeline.
3
3
  * Implements spec/04_TYPE_SYSTEM.md.
4
4
  *
@@ -180,6 +180,7 @@ export class TypeChecker {
180
180
  case "ExtensionPointDecl": this.checkExtensionPoint(decl); break;
181
181
  case "StoreDecl": this.checkStore(decl); break;
182
182
  case "PolicyDecl": this.checkPolicy(decl); break;
183
+ case "EventDecl": this.checkEvent(decl); break;
183
184
  }
184
185
  }
185
186
 
@@ -524,8 +525,22 @@ export class TypeChecker {
524
525
  if (condType && !this.isBoolish(condType)) {
525
526
  this.addError("T005", "Ternary condition must be bool", expr.loc);
526
527
  }
527
- // result type is type of consequent (assume both branches same type)
528
- return this.inferExprType(expr.consequent, ctx);
528
+ // Both branches must have compatible types
529
+ const consequentType = this.inferExprType(expr.consequent, ctx);
530
+ const alternateType = this.inferExprType(expr.alternate, ctx);
531
+ if (
532
+ consequentType && alternateType &&
533
+ !typeEquals(consequentType, alternateType) &&
534
+ !this.isAssignable(consequentType, alternateType) &&
535
+ !this.isAssignable(alternateType, consequentType)
536
+ ) {
537
+ this.addError(
538
+ "T017",
539
+ `Ternary branches have incompatible types: consequent is ${typeToString(consequentType)}, alternate is ${typeToString(alternateType)}`,
540
+ expr.loc,
541
+ );
542
+ }
543
+ return consequentType;
529
544
  }
530
545
 
531
546
  // ─── Helpers ───────────────────────────────────────────────────────────────
@@ -635,10 +650,38 @@ export class TypeChecker {
635
650
  }
636
651
  }
637
652
 
653
+ // ─── Event Checking ───────────────────────────────────────────────────────────
638
654
 
639
- }
655
+ private static readonly VALID_DELIVERY_MODES = new Set(["at_least_once", "at_most_once", "exactly_once"]);
656
+
657
+ private checkEvent(decl: AST.EventDeclNode): void {
658
+ // Check payload field types resolve and have no duplicates
659
+ const seen = new Set<string>();
660
+ for (const field of decl.payload) {
661
+ if (seen.has(field.name)) {
662
+ this.addError("T009", `Duplicate field name '${field.name}' in event '${decl.name}'`, field.loc);
663
+ }
664
+ seen.add(field.name);
665
+
666
+ const resolved = this.resolveTypeExpr(field.type);
667
+ if (!resolved) {
668
+ this.addError("T001", `Event '${decl.name}' payload field '${field.name}' references undefined type`, field.loc);
669
+ }
670
+ }
671
+
672
+ // Check delivery mode is valid
673
+ if (decl.delivery && !TypeChecker.VALID_DELIVERY_MODES.has(decl.delivery)) {
674
+ this.addError(
675
+ "T016",
676
+ `Event '${decl.name}' has invalid delivery mode '${decl.delivery}'. ` +
677
+ `Valid values: ${[...TypeChecker.VALID_DELIVERY_MODES].join(", ")}.`,
678
+ decl.loc,
679
+ );
680
+ }
681
+ }
640
682
  // ─── Type Context ────────────────────────────────────────────────────────────
641
683
 
684
+ }
642
685
  class TypeContext {
643
686
  private locals: Map<string, CVType>;
644
687
  private symbols: SymbolTable;
package/src/verifier.ts CHANGED
@@ -1,4 +1,4 @@
1
- /**
1
+ /**
2
2
  * BoneScript Verifier — Stage 7 of the compilation pipeline.
3
3
  * Implements spec/07_IR_SPEC.md §5 (IR Validation Rules).
4
4
  *
@@ -197,7 +197,7 @@ export class Verifier {
197
197
  const visited = new Set<string>();
198
198
  const inStack = new Set<string>();
199
199
 
200
- const dfs = (node: string, path: string[]): boolean => {
200
+ const dfs = (node: string, path: string[]): void => {
201
201
  if (inStack.has(node)) {
202
202
  const cycle = [...path.slice(path.indexOf(node)), node];
203
203
  const names = cycle.map(id => system.modules.find(m => m.id === id)?.name || id);
@@ -207,9 +207,9 @@ export class Verifier {
207
207
  message: `Circular dependency: ${names.join(" → ")}`,
208
208
  location: node,
209
209
  });
210
- return true;
210
+ return;
211
211
  }
212
- if (visited.has(node)) return false;
212
+ if (visited.has(node)) return;
213
213
 
214
214
  visited.add(node);
215
215
  inStack.add(node);
@@ -221,7 +221,6 @@ export class Verifier {
221
221
  }
222
222
 
223
223
  inStack.delete(node);
224
- return false;
225
224
  };
226
225
 
227
226
  for (const [id] of graph) {