1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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
62
63
64
65
66
67
|
#!/usr/bin/env node
// Build tool only: reuse the existing authoring compiler. Its parser, JS runtime
// and source syntax do not become dependencies of the resulting native app.
import { readFile, writeFile } from 'node:fs/promises';
import { createHash } from 'node:crypto';
import { compile } from '../web/compiler.mjs';
const [input, output, profile] = process.argv.slice(2);
if (!input || !output || ![4, 5].includes(process.argv.length))
throw new Error('prepare.mjs SOURCE.pure OUTPUT.c [FIXED_PROFILE.zig]');
const source = await readFile(input, 'utf8');
const p = compile(source);
// Encode strings as byte escapes, not JSON/C escape substitution. No source text
// or user-authored identifier can become executable C through this preparation.
const string = s => '"' + [...Buffer.from(s ?? '', 'utf8')].map(b => `\\${b.toString(8).padStart(3, '0')}`).join('') + '"';
const number = n => Object.is(n, -0) ? '-0.0' : Number.isFinite(n) ? String(n) : (() => { throw new Error('nonfinite prepared constant'); })();
const declaration = (type, name, rows) => `static const ${type} ${name}[] = {\n${rows.length ? rows.map(r => ` ${r},`).join('\n') : ' {0},'}\n};\n`;
let text = '#include "plan.h"\n\n';
text += declaration('PureScalarState', 'states', p.states.map(s => `{${string(s.name)}, ${number(s.initial)}}`));
text += declaration('PureScalarInput', 'inputs', p.inputs.map(i => `{${string(i.name)}, ${string(i.key)}, ${string(i.target)}, ${i.kind === 'value' ? 'PURE_VALUE' : i.routes ? 'PURE_ROUTED' : i.target ? 'PURE_POINT' : 'PURE_KEY'}}`));
text += '#ifndef PURE_FIXED_DEFINITION\n';
text += declaration('PureScalarInstruction', 'instructions', p.instructions.map(i => `{${i.op}, ${i.arg}, ${number(i.value)}}`));
text += declaration('PureScalarHandler', 'handlers', p.handlers.map(h => `{${h.event}, ${h.start}, ${h.length}, ${h.parameterBase ?? 0}, ${h.parameterCount ?? 0}}`));
text += '#endif\n';
text += declaration('PureScalarTarget', 'targets', p.pointerTargets.map((t, index) => {
const route = p.inputs[t.input].routes.find(r => r.route === index);
if (!route) throw new Error('missing prepared pointer target');
return `{${t.input}, ${t.base}, ${t.count}, ${string(route.target)}}`;
}));
text += declaration('PureScalarView', 'views', p.views.map(v => {
const fields = Object.values(v.bindings);
return `{${string(v.id)}, ${string(v.type)}, ${string(v.color ?? v.prefix)}, ${fields.length}, {${fields.map(b => `{${b.start}, ${b.length}}`).join(', ')}}}`;
}));
// Definition identity protects restore compatibility, not authenticity. Persisted
// values from a different definition are rejected; storage authentication is separate.
const identity = createHash('sha256').update(JSON.stringify(p)).digest();
text += '#ifdef PURE_FIXED_DEFINITION\n#define PURE_INSTRUCTIONS 0\n#define PURE_HANDLERS 0\n#else\n#define PURE_INSTRUCTIONS instructions\n#define PURE_HANDLERS handlers\n#endif\n';
text += `\nconst PureScalarPlan pure_app_plan = {\n ${p.states.length}, ${p.inputs.length}, ${p.instructions.length}, ${p.handlers.length}, ${p.pointerTargets.length}, ${p.views.length},\n states, inputs, PURE_INSTRUCTIONS, PURE_HANDLERS, targets, views,\n {${[...identity].join(', ')}}\n};\n`;
await writeFile(output, text);
if (profile) {
// A fixed CLI/peer has no source-replacement path. These values size the
// existing VM arrays; GTK and Wasm keep the full authoring capacities.
const limits = [
['states', p.states.length], ['inputs', p.inputs.length],
['instructions', p.instructions.length], ['handlers', p.handlers.length],
['pointer_targets', p.pointerTargets.length],
];
// Zig rejects an index operation on a statically empty array even when a
// preceding runtime bound check makes that operation unreachable.
const profileText = [
...limits.map(([name, count]) => `pub const ${name} = ${Math.max(1, count)};`),
...limits.map(([name, count]) => `pub const ${name}_count = ${count};`),
`pub const identity = [_]u8{${[...identity].join(',')}};`,
`pub const initials = [_]f64{${p.states.map(s => number(s.initial)).join(',')}};`,
'pub const Instruction = struct { op: u32, arg: u32, value: f64 };',
`pub const code = [_]Instruction{${p.instructions.map(i =>
`.{.op=${i.op},.arg=${i.arg},.value=${number(i.value)}}`).join(',')}};`,
'pub const Handler = struct { event: u32, start: u32, length: u32, base: u8, count: u8 };',
`pub const handlers_data = [_]Handler{${p.handlers.map(h =>
`.{.event=${h.event},.start=${h.start},.length=${h.length},.base=${h.parameterBase ?? 0},.count=${h.parameterCount ?? 0}}`).join(',')}};`,
'pub const Target = struct { input: u8, base: u8, count: u8 };',
`pub const targets_data = [_]Target{${p.pointerTargets.map(t =>
`.{.input=${t.input},.base=${t.base},.count=${t.count}}`).join(',')}};`,
`pub const routed_inputs = ${p.pointerTargets.reduce((mask, t) => mask | (1 << t.input), 0)};`,
].join('\n') + '\n';
await writeFile(profile, profileText);
}
|