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.
- package/dist/commands/diff.js +3 -5
- package/dist/commands/diff.js.map +1 -1
- package/dist/emit_capability.js +6 -8
- package/dist/emit_capability.js.map +1 -1
- package/dist/emit_database.js +2 -4
- package/dist/emit_database.js.map +1 -1
- package/dist/emit_deploy.js +6 -8
- package/dist/emit_deploy.js.map +1 -1
- package/dist/emit_events.js +2 -19
- package/dist/emit_events.js.map +1 -1
- package/dist/emit_extras.js +3 -5
- package/dist/emit_extras.js.map +1 -1
- package/dist/emit_full.js +9 -11
- package/dist/emit_full.js.map +1 -1
- package/dist/emit_openapi.js +2 -4
- package/dist/emit_openapi.js.map +1 -1
- package/dist/emit_package.js +2 -4
- package/dist/emit_package.js.map +1 -1
- package/dist/emit_router.js +3 -4
- package/dist/emit_router.js.map +1 -1
- package/dist/emit_tests.js +16 -18
- package/dist/emit_tests.js.map +1 -1
- package/dist/emit_websocket.js +0 -3
- package/dist/emit_websocket.js.map +1 -1
- package/dist/emitter.js +27 -48
- package/dist/emitter.js.map +1 -1
- package/dist/extension_manager.js +3 -17
- package/dist/extension_manager.js.map +1 -1
- package/dist/optimizer.js +6 -3
- package/dist/optimizer.js.map +1 -1
- package/dist/solver.js +1 -1
- package/dist/solver.js.map +1 -1
- package/dist/typechecker.d.ts +2 -0
- package/dist/typechecker.js +35 -3
- package/dist/typechecker.js.map +1 -1
- package/dist/verifier.js +2 -3
- package/dist/verifier.js.map +1 -1
- package/package.json +3 -3
- package/src/commands/compile.ts +191 -191
- package/src/commands/diff.ts +1 -4
- package/src/emit_capability.ts +1 -5
- package/src/emit_database.ts +1 -4
- package/src/emit_deploy.ts +1 -4
- package/src/emit_events.ts +1 -16
- package/src/emit_extras.ts +1 -4
- package/src/emit_full.ts +1 -4
- package/src/emit_openapi.ts +1 -4
- package/src/emit_package.ts +1 -4
- package/src/emit_router.ts +1 -2
- package/src/emit_tests.ts +1 -4
- package/src/emit_websocket.ts +1 -4
- package/src/emitter.ts +12 -28
- package/src/extension_manager.ts +1 -13
- package/src/optimizer.ts +6 -3
- package/src/solver.ts +1 -1
- package/src/typechecker.ts +47 -4
- package/src/verifier.ts +4 -5
package/src/emit_capability.ts
CHANGED
|
@@ -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>();
|
package/src/emit_database.ts
CHANGED
|
@@ -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);
|
package/src/emit_deploy.ts
CHANGED
|
@@ -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.
|
package/src/emit_events.ts
CHANGED
|
@@ -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
|
|
package/src/emit_extras.ts
CHANGED
|
@@ -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" {
|
package/src/emit_openapi.ts
CHANGED
|
@@ -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 {
|
package/src/emit_package.ts
CHANGED
|
@@ -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 = {
|
package/src/emit_router.ts
CHANGED
|
@@ -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[] = [];
|
package/src/emit_websocket.ts
CHANGED
|
@@ -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
|
-
//
|
|
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: "
|
|
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
|
package/src/extension_manager.ts
CHANGED
|
@@ -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
|
|
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"
|
|
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: "
|
|
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",
|
package/src/typechecker.ts
CHANGED
|
@@ -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
|
-
//
|
|
528
|
-
|
|
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[]):
|
|
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
|
|
210
|
+
return;
|
|
211
211
|
}
|
|
212
|
-
if (visited.has(node)) return
|
|
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) {
|