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.
- package/LICENSE +21 -21
- package/dist/algorithm_catalog.js +166 -166
- package/dist/cli.d.ts +2 -1
- package/dist/cli.js +75 -543
- package/dist/cli.js.map +1 -1
- package/dist/commands/check.d.ts +5 -0
- package/dist/commands/check.js +34 -0
- package/dist/commands/check.js.map +1 -0
- package/dist/commands/compile.d.ts +5 -0
- package/dist/commands/compile.js +215 -0
- package/dist/commands/compile.js.map +1 -0
- package/dist/commands/debug.d.ts +5 -0
- package/dist/commands/debug.js +59 -0
- package/dist/commands/debug.js.map +1 -0
- package/dist/commands/diff.d.ts +5 -0
- package/dist/commands/diff.js +125 -0
- package/dist/commands/diff.js.map +1 -0
- package/dist/commands/fmt.d.ts +5 -0
- package/dist/commands/fmt.js +49 -0
- package/dist/commands/fmt.js.map +1 -0
- package/dist/commands/init.d.ts +5 -0
- package/dist/commands/init.js +96 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/ir.d.ts +5 -0
- package/dist/commands/ir.js +27 -0
- package/dist/commands/ir.js.map +1 -0
- package/dist/commands/lex.d.ts +5 -0
- package/dist/commands/lex.js +21 -0
- package/dist/commands/lex.js.map +1 -0
- package/dist/commands/parse.d.ts +5 -0
- package/dist/commands/parse.js +30 -0
- package/dist/commands/parse.js.map +1 -0
- package/dist/commands/test.d.ts +5 -0
- package/dist/commands/test.js +61 -0
- package/dist/commands/test.js.map +1 -0
- package/dist/commands/verify_determinism.d.ts +5 -0
- package/dist/commands/verify_determinism.js +64 -0
- package/dist/commands/verify_determinism.js.map +1 -0
- package/dist/commands/watch.d.ts +5 -0
- package/dist/commands/watch.js +50 -0
- package/dist/commands/watch.js.map +1 -0
- package/dist/emit_auth.d.ts +6 -0
- package/dist/emit_auth.js +69 -0
- package/dist/emit_auth.js.map +1 -0
- package/dist/emit_capability.d.ts +13 -0
- package/dist/emit_capability.js +292 -128
- package/dist/emit_capability.js.map +1 -1
- package/dist/emit_composition.js +37 -3
- package/dist/emit_composition.js.map +1 -1
- package/dist/emit_database.d.ts +7 -0
- package/dist/emit_database.js +74 -0
- package/dist/emit_database.js.map +1 -0
- package/dist/emit_deploy.js +162 -162
- package/dist/emit_events.d.ts +1 -0
- package/dist/emit_events.js +342 -275
- package/dist/emit_events.js.map +1 -1
- package/dist/emit_full.js +135 -95
- package/dist/emit_full.js.map +1 -1
- package/dist/emit_index.d.ts +6 -0
- package/dist/emit_index.js +157 -0
- package/dist/emit_index.js.map +1 -0
- package/dist/emit_maintenance.js +249 -249
- package/dist/emit_models.d.ts +12 -0
- package/dist/emit_models.js +171 -0
- package/dist/emit_models.js.map +1 -0
- package/dist/emit_openapi.d.ts +9 -0
- package/dist/emit_openapi.js +308 -0
- package/dist/emit_openapi.js.map +1 -0
- package/dist/emit_package.d.ts +7 -0
- package/dist/emit_package.js +70 -0
- package/dist/emit_package.js.map +1 -0
- package/dist/emit_router.d.ts +12 -0
- package/dist/emit_router.js +390 -0
- package/dist/emit_router.js.map +1 -0
- package/dist/emit_runtime.d.ts +17 -11
- package/dist/emit_runtime.js +29 -686
- package/dist/emit_runtime.js.map +1 -1
- package/dist/emit_sourcemap.js +66 -66
- package/dist/emit_tests.js +37 -0
- package/dist/emit_tests.js.map +1 -1
- package/dist/emitter.js +34 -5
- package/dist/emitter.js.map +1 -1
- package/dist/extension_manager.d.ts +2 -2
- package/dist/extension_manager.js +6 -3
- package/dist/extension_manager.js.map +1 -1
- package/dist/lowering.d.ts +5 -14
- package/dist/lowering.js +47 -417
- package/dist/lowering.js.map +1 -1
- package/dist/lowering_channels.d.ts +11 -0
- package/dist/lowering_channels.js +102 -0
- package/dist/lowering_channels.js.map +1 -0
- package/dist/lowering_entities.d.ts +11 -0
- package/dist/lowering_entities.js +222 -0
- package/dist/lowering_entities.js.map +1 -0
- package/dist/lowering_helpers.d.ts +13 -0
- package/dist/lowering_helpers.js +76 -0
- package/dist/lowering_helpers.js.map +1 -0
- package/dist/module_loader.d.ts +2 -2
- package/dist/module_loader.js +20 -23
- package/dist/module_loader.js.map +1 -1
- package/dist/scaffold.d.ts +2 -2
- package/dist/scaffold.js +316 -319
- package/dist/scaffold.js.map +1 -1
- package/dist/typechecker.js +32 -13
- package/dist/typechecker.js.map +1 -1
- package/dist/verifier.d.ts +5 -0
- package/dist/verifier.js +140 -2
- package/dist/verifier.js.map +1 -1
- package/package.json +62 -52
- package/src/algorithm_catalog.ts +345 -345
- package/src/ast.ts +334 -334
- package/src/cli.ts +98 -624
- package/src/commands/check.ts +33 -0
- package/src/commands/compile.ts +191 -0
- package/src/commands/debug.ts +33 -0
- package/src/commands/diff.ts +108 -0
- package/src/commands/fmt.ts +22 -0
- package/src/commands/init.ts +72 -0
- package/src/commands/ir.ts +23 -0
- package/src/commands/lex.ts +17 -0
- package/src/commands/parse.ts +24 -0
- package/src/commands/test.ts +36 -0
- package/src/commands/verify_determinism.ts +66 -0
- package/src/commands/watch.ts +25 -0
- package/src/emit_auth.ts +67 -0
- package/src/emit_batch.ts +140 -140
- package/src/emit_capability.ts +617 -436
- package/src/emit_composition.ts +229 -196
- package/src/emit_database.ts +75 -0
- package/src/emit_deploy.ts +190 -190
- package/src/emit_events.ts +377 -307
- package/src/emit_extras.ts +240 -240
- package/src/emit_full.ts +351 -309
- package/src/emit_index.ts +161 -0
- package/src/emit_maintenance.ts +459 -459
- package/src/emit_models.ts +176 -0
- package/src/emit_openapi.ts +318 -0
- package/src/emit_package.ts +69 -0
- package/src/emit_router.ts +409 -0
- package/src/emit_runtime.ts +17 -728
- package/src/emit_sourcemap.ts +140 -140
- package/src/emit_tests.ts +246 -205
- package/src/emit_websocket.ts +229 -229
- package/src/emitter.ts +31 -5
- package/src/extension_manager.ts +189 -187
- package/src/formatter.ts +297 -297
- package/src/index.ts +88 -88
- package/src/ir.ts +215 -215
- package/src/lexer.ts +630 -630
- package/src/lowering.ts +142 -556
- package/src/lowering_channels.ts +107 -0
- package/src/lowering_entities.ts +248 -0
- package/src/lowering_helpers.ts +75 -0
- package/src/module_loader.ts +112 -114
- package/src/optimizer.ts +196 -196
- package/src/parse_decls.ts +409 -409
- package/src/parse_decls2.ts +244 -244
- package/src/parse_expr.ts +197 -197
- package/src/parse_types.ts +54 -54
- package/src/parser.ts +1 -1
- package/src/parser_base.ts +57 -57
- package/src/parser_recovery.ts +153 -153
- package/src/scaffold.ts +372 -375
- package/src/solver.ts +330 -330
- package/src/typechecker.ts +30 -15
- package/src/types.ts +122 -122
- 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
|
+
}
|
package/src/module_loader.ts
CHANGED
|
@@ -1,114 +1,112 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* BoneScript Module Loader
|
|
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 {
|
|
14
|
-
import {
|
|
15
|
-
import
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
private
|
|
26
|
-
private
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
return {
|
|
97
|
-
...sys,
|
|
98
|
-
declarations: [...sys.declarations.filter(d => d.kind !== "ImportDecl"), ...importedDecls],
|
|
99
|
-
};
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
+
}
|