stepper-sdk 0.1.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.
@@ -0,0 +1,128 @@
1
+ /**
2
+ * verify.js — replay a chip's whole history and check the chain agrees.
3
+ *
4
+ * This is the one function in the package that is worth installing it for.
5
+ *
6
+ * A STEPPER chip is deterministic and its inputs are all public. `program()`
7
+ * returns the ROM. Every `step()` emits `Stepped(sponsor, cycle, outPort,
8
+ * inValue)`, so the log carries the byte that went in and the cycle it went in
9
+ * on. ROM plus those events is the complete tape a deterministic machine ran
10
+ * on — which means the entire history can be recomputed from scratch by
11
+ * anybody, on their own machine, and compared against what the contract now
12
+ * says about itself.
13
+ *
14
+ * Two claims are checked, and the second is the stronger one:
15
+ *
16
+ * The end state matches. Cycle, program counter, output port and all three
17
+ * flags, against `snapshot()`.
18
+ *
19
+ * Every intermediate step matches. The output port the contract logged on
20
+ * cycle N is compared with the output port this netlist produces on cycle N,
21
+ * for every N. A chip whose final state happened to agree but whose history
22
+ * did not would fail here, and that is exactly the chip worth catching.
23
+ *
24
+ * Nothing in here trusts us. The netlist is in the package, the events come
25
+ * from whatever node the caller names, and the arithmetic happens locally. A
26
+ * false answer would require the reader's own copy of the processor to be
27
+ * wrong, which is what `npm run silicon` exists to rule out.
28
+ */
29
+
30
+ "use strict";
31
+
32
+ var machine = require("../netlist/machine.js");
33
+ var chain = require("./chain.js");
34
+
35
+ /**
36
+ * @param {string} address the chip
37
+ * @param {object} [opts] { rpc, window, fromBlock }
38
+ * @returns {Promise<object>} the verdict, with everything it rests on
39
+ */
40
+ async function verify(address, opts) {
41
+ var o = opts || {};
42
+
43
+ var chip = await chain.readChip(address, o);
44
+ var logs = await chain.history(address, {
45
+ rpc: o.rpc,
46
+ window: o.window,
47
+ fromBlock: o.fromBlock,
48
+ expect: chip.cycle,
49
+ });
50
+
51
+ var problems = [];
52
+
53
+ /* A machine that has taken N cycles emitted N events. Fewer means the scan
54
+ did not reach far enough back or the chain is missing something; more
55
+ means something is emitting events that are not cycles. Either way the
56
+ replay below would be running on the wrong tape, so it is refused rather
57
+ than reported as a mismatch. */
58
+ if (logs.length !== chip.cycle) {
59
+ problems.push(chip.cycle + " cycles on chain but " + logs.length +
60
+ " Stepped events found");
61
+ }
62
+
63
+ /* The cycles must be 1..N with nothing missing and nothing twice. */
64
+ for (var i = 0; i < logs.length; i++) {
65
+ if (logs[i].cycle !== i + 1) {
66
+ problems.push("cycle " + (i + 1) + " is missing or out of order");
67
+ break;
68
+ }
69
+ }
70
+
71
+ if (problems.length) {
72
+ return {
73
+ ok: false, address: address, onChain: chip,
74
+ cyclesReplayed: 0, events: logs.length, problems: problems,
75
+ };
76
+ }
77
+
78
+ /* The replay. One event, one clock edge, in the order they were mined. */
79
+ var D = machine.loadNetlist();
80
+ var m = new machine.Machine(D, chip.rom);
81
+ var firstMismatch = null;
82
+
83
+ for (i = 0; i < logs.length; i++) {
84
+ m.inPort = logs[i].inValue;
85
+ m.step();
86
+ if (firstMismatch === null && m.out() !== logs[i].outPort) {
87
+ firstMismatch = {
88
+ cycle: logs[i].cycle,
89
+ chain: logs[i].outPort,
90
+ replay: m.out(),
91
+ tx: logs[i].tx,
92
+ };
93
+ }
94
+ }
95
+
96
+ if (firstMismatch) {
97
+ problems.push("cycle " + firstMismatch.cycle + ": the chain logged output " +
98
+ firstMismatch.chain + ", this netlist produces " + firstMismatch.replay);
99
+ }
100
+
101
+ /* And the state the contract reports right now. */
102
+ var ended = {
103
+ cycle: m.cycle, pc: m.pc(), out: m.out(),
104
+ carry: !!m.carry(), zero: !!m.zero(), halted: m.halted(),
105
+ };
106
+ ["cycle", "pc", "out", "carry", "zero", "halted"].forEach(function (k) {
107
+ if (ended[k] !== chip[k]) {
108
+ problems.push("final " + k + ": chain says " + chip[k] + ", replay says " + ended[k]);
109
+ }
110
+ });
111
+
112
+ return {
113
+ ok: problems.length === 0,
114
+ address: address,
115
+ cyclesReplayed: logs.length,
116
+ events: logs.length,
117
+ gates: D.gateCount,
118
+ onChain: {
119
+ cycle: chip.cycle, pc: chip.pc, out: chip.out,
120
+ carry: chip.carry, zero: chip.zero, halted: chip.halted,
121
+ },
122
+ replayed: ended,
123
+ firstMismatch: firstMismatch,
124
+ problems: problems,
125
+ };
126
+ }
127
+
128
+ module.exports = { verify: verify };
@@ -0,0 +1,184 @@
1
+ /**
2
+ * asm.js — the ST-8 assembler, and the two programs the site ships.
3
+ *
4
+ * A word is 25 bits: [24:20] op · [19:16] rd · [15:12] rs · [11:0] imm/addr.
5
+ * Nothing clever happens here — one line becomes one word — but the listing
6
+ * it produces is what the ROM panel on the site renders, so the assembler is
7
+ * also the thing that keeps the listing honest about what is executing.
8
+ */
9
+
10
+ "use strict";
11
+
12
+ var OP = require("./st8.js").OP;
13
+
14
+ var FORMS = {
15
+ nop: "none", hlt: "none",
16
+ not: "rd", shl: "rd", shr: "rd", rol: "rd", ror: "rd",
17
+ inc: "rd", dec: "rd", in: "rd", out: "rd",
18
+ mov: "rr", add: "rr", adc: "rr", sub: "rr", sbb: "rr",
19
+ and: "rr", or: "rr", xor: "rr", nand: "rr", cmp: "rr",
20
+ ldi: "imm",
21
+ ld: "load", st: "store",
22
+ jmp: "addr", jz: "addr", jnz: "addr", jc: "addr", jnc: "addr",
23
+ tst: "rr", swap: "rd", jmpr: "rd",
24
+ };
25
+
26
+ function reg(tok, line) {
27
+ var m = /^r(\d{1,2})$/i.exec(String(tok).trim());
28
+ if (!m) throw new Error("line " + line + ": expected a register, got '" + tok + "'");
29
+ var n = +m[1];
30
+ if (n > 15) throw new Error("line " + line + ": r" + n + " does not exist");
31
+ return n;
32
+ }
33
+
34
+ function imm(tok, line, max) {
35
+ var t = String(tok).trim().replace(/^#/, "");
36
+ var n = /^0x/i.test(t) ? parseInt(t, 16) : parseInt(t, 10);
37
+ if (!Number.isFinite(n)) throw new Error("line " + line + ": bad number '" + tok + "'");
38
+ if (n < 0 || n > max) throw new Error("line " + line + ": " + n + " out of range 0.." + max);
39
+ return n;
40
+ }
41
+
42
+ /**
43
+ * Two passes: collect labels, then encode. A label may be used before it is
44
+ * defined, which is the only reason a forward jump is writable at all.
45
+ */
46
+ function assemble(source) {
47
+ var raw = source.split("\n");
48
+ var labels = {};
49
+ var stmts = [];
50
+
51
+ raw.forEach(function (text, n) {
52
+ var line = text.replace(/;.*$/, "").trim();
53
+ while (line) {
54
+ var lm = /^([A-Za-z_][A-Za-z0-9_]*)\s*:\s*/.exec(line);
55
+ if (!lm) break;
56
+ labels[lm[1]] = stmts.length;
57
+ line = line.slice(lm[0].length).trim();
58
+ }
59
+ if (line) stmts.push({ text: line, line: n + 1 });
60
+ });
61
+
62
+ if (stmts.length > 1024) throw new Error("program exceeds 1,024 words of ROM");
63
+
64
+ var rom = new Array(1024).fill(0);
65
+ var listing = [];
66
+
67
+ stmts.forEach(function (st, pc) {
68
+ var m = /^([A-Za-z]+)\s*(.*)$/.exec(st.text);
69
+ if (!m) throw new Error("line " + st.line + ": cannot parse '" + st.text + "'");
70
+ var mn = m[1].toLowerCase();
71
+ var rest = m[2].trim();
72
+ var form = FORMS[mn];
73
+ if (!form) throw new Error("line " + st.line + ": unknown instruction '" + mn + "'");
74
+
75
+ var op = OP[mn.toUpperCase()];
76
+ var rd = 0, rs = 0, field = 0, mm;
77
+
78
+ if (form === "rd") {
79
+ rd = reg(rest, st.line);
80
+ } else if (form === "rr") {
81
+ mm = rest.split(",");
82
+ if (mm.length !== 2) throw new Error("line " + st.line + ": expected 'rd, rs'");
83
+ rd = reg(mm[0], st.line);
84
+ rs = reg(mm[1], st.line);
85
+ } else if (form === "imm") {
86
+ mm = rest.split(",");
87
+ if (mm.length !== 2) throw new Error("line " + st.line + ": expected 'rd, #imm'");
88
+ rd = reg(mm[0], st.line);
89
+ field = imm(mm[1], st.line, 255);
90
+ } else if (form === "load") {
91
+ mm = /^(r\d{1,2})\s*,\s*\[\s*(r\d{1,2})\s*\]$/i.exec(rest);
92
+ if (!mm) throw new Error("line " + st.line + ": expected 'ld rd, [rs]'");
93
+ rd = reg(mm[1], st.line);
94
+ rs = reg(mm[2], st.line);
95
+ } else if (form === "store") {
96
+ mm = /^\[\s*(r\d{1,2})\s*\]\s*,\s*(r\d{1,2})$/i.exec(rest);
97
+ if (!mm) throw new Error("line " + st.line + ": expected 'st [rd], rs'");
98
+ rd = reg(mm[1], st.line);
99
+ rs = reg(mm[2], st.line);
100
+ } else if (form === "addr") {
101
+ if (Object.prototype.hasOwnProperty.call(labels, rest)) field = labels[rest];
102
+ else field = imm(rest, st.line, 1023);
103
+ }
104
+
105
+ var word = ((op & 0x1f) << 20) | ((rd & 0xf) << 16) | ((rs & 0xf) << 12) | (field & 0xfff);
106
+ rom[pc] = word >>> 0;
107
+ listing.push({
108
+ pc: pc,
109
+ hex: word.toString(16).padStart(7, "0"),
110
+ src: st.text.replace(/\s+/g, " "),
111
+ });
112
+ });
113
+
114
+ return { rom: rom, labels: labels, listing: listing };
115
+ }
116
+
117
+ /* --------------------------------------------------------------- programs */
118
+
119
+ /**
120
+ * ledger — what chip #1 carries, and it runs forever.
121
+ *
122
+ * The output port shows whatever byte the sponsor of this cycle sent, r2 holds
123
+ * the sum of every byte the chip was ever given, and RAM keeps the trace. So
124
+ * what the machine accumulates is a record of who has been paying to run it,
125
+ * which is the reason it is called a ledger rather than an echo.
126
+ *
127
+ * It must never halt, so there is no `hlt` in it and every path returns to
128
+ * the loop. The build refuses to ship if it ever stops.
129
+ */
130
+ var LEDGER = [
131
+ " ldi r2, #0 ; running sum of every byte ever sent",
132
+ " ldi r3, #0 ; where in RAM the next sample lands",
133
+ "loop: in r0 ; the byte whoever paid for this step sent",
134
+ " out r0 ; echo it straight back to the output port",
135
+ " add r2, r0 ; fold it into the sum",
136
+ " mov r1, r2",
137
+ " st [r3], r1 ; keep a trace of it in RAM",
138
+ " inc r3 ; wrap at 256 and start overwriting",
139
+ " jmp loop",
140
+ ].join("\n");
141
+
142
+ /**
143
+ * selftest — exercises the ALU, both flags, the shifter and a RAM round trip,
144
+ * then halts on purpose. Any failure jumps to `fail`, which puts 255 on the
145
+ * output port, so a wrong answer is visible on the LEDs rather than silent.
146
+ */
147
+ var SELFTEST = [
148
+ " ldi r0, #200",
149
+ " ldi r1, #100",
150
+ " add r0, r1 ; 300 wraps to 44 and sets carry",
151
+ " jnc fail",
152
+ " out r0",
153
+ " ldi r2, #44",
154
+ " cmp r0, r2 ; equal, so Z",
155
+ " jnz fail",
156
+ " ldi r3, #0xf0",
157
+ " ldi r4, #0x0f",
158
+ " or r3, r4 ; 0xff",
159
+ " out r3",
160
+ " not r3 ; 0x00, so Z",
161
+ " jnz fail",
162
+ " ldi r5, #1",
163
+ " shl r5",
164
+ " shl r5 ; 4",
165
+ " ldi r6, #4",
166
+ " cmp r5, r6",
167
+ " jnz fail",
168
+ " ldi r7, #16",
169
+ " ldi r8, #123",
170
+ " st [r7], r8 ; write it",
171
+ " ld r9, [r7] ; read it back — two honest cycles",
172
+ " cmp r9, r8",
173
+ " jnz fail",
174
+ " out r9",
175
+ " hlt",
176
+ "fail: ldi r0, #255",
177
+ " out r0",
178
+ " hlt",
179
+ ].join("\n");
180
+
181
+ module.exports = {
182
+ assemble: assemble,
183
+ sources: { ledger: LEDGER, selftest: SELFTEST },
184
+ };
@@ -0,0 +1,116 @@
1
+ /**
2
+ * machine.js — the ST-8 netlist, evaluated.
3
+ *
4
+ * This is the processor as a library: load the gate table, drive the inputs,
5
+ * evaluate every gate in topological order, latch every flip-flop at once.
6
+ * Nothing here prints, exits, or reads an argument.
7
+ *
8
+ * It lives on its own rather than inside the runner because three things need
9
+ * it now — the terminal runner, the published CLI and the SDK — and a
10
+ * processor that exists in three copies is three processors. Only one of them
11
+ * would be the one `npm run silicon` verified.
12
+ */
13
+
14
+ "use strict";
15
+
16
+ var fs = require("node:fs");
17
+ var path = require("node:path");
18
+
19
+ /**
20
+ * st8-data.js is written for a browser, so it assigns to `window`. Handing it
21
+ * an object called window is the whole of the port.
22
+ *
23
+ * It is looked for in every place this file is ever installed: under public/
24
+ * in the repository, and beside lib/ in a published package. Searching costs
25
+ * one stat per candidate and saves keeping a second loader in step.
26
+ */
27
+ function loadNetlist() {
28
+ var places = [
29
+ path.join(__dirname, "..", "..", "public", "scripts", "st8-data.js"),
30
+ path.join(__dirname, "..", "st8-data.js"),
31
+ path.join(__dirname, "st8-data.js"),
32
+ ];
33
+ var file = places.filter(function (p) { return fs.existsSync(p); })[0];
34
+ if (!file) {
35
+ throw new Error("st8-data.js is missing. Run `npm run silicon` first.");
36
+ }
37
+ var win = {};
38
+ new Function("window", fs.readFileSync(file, "utf8"))(win);
39
+ return win.ST8_DATA;
40
+ }
41
+ /* ------------------------------------------------------------- the machine
42
+ *
43
+ * Identical in behaviour to public/scripts/machine.js: drive the inputs,
44
+ * evaluate every gate in the order the topological sort fixed, then latch
45
+ * every flip-flop at once. Kept as a separate copy on purpose: if the two
46
+ * ever disagree, one of them is wrong, and `npm run silicon` checks them
47
+ * against a third independent model on every build.
48
+ */
49
+
50
+ function Machine(D, rom) {
51
+ this.D = D;
52
+ this.rom = rom;
53
+ this.v = new Uint8Array(D.nets);
54
+ this.next = new Uint8Array(D.flopCount);
55
+ this.ram = new Uint8Array(256);
56
+ this.inPort = 0;
57
+ this.cycle = 0;
58
+ this.switched = 0;
59
+ this.v[D.one] = 1;
60
+ }
61
+
62
+ Machine.prototype.flop = function (i) { return this.v[this.D.flops[2 * i + 1]]; };
63
+ Machine.prototype.field = function (bits) {
64
+ let n = 0;
65
+ for (let i = 0; i < bits.length; i++) n |= this.flop(bits[i]) << i;
66
+ return n;
67
+ };
68
+ Machine.prototype.pc = function () { return this.field(this.D.pc); };
69
+ Machine.prototype.out = function () { return this.field(this.D.out); };
70
+ Machine.prototype.reg = function (r) { return this.field(this.D.regs[r]); };
71
+ Machine.prototype.carry = function () { return this.flop(this.D.cf); };
72
+ Machine.prototype.zero = function () { return this.flop(this.D.zf); };
73
+ Machine.prototype.halted = function () { return this.flop(this.D.halt) === 1; };
74
+
75
+ Machine.prototype.step = function () {
76
+ const D = this.D, v = this.v;
77
+ const word = this.rom[this.pc()] || 0;
78
+ let i;
79
+
80
+ for (i = 0; i < 25; i++) v[D.instr[i]] = (word >> i) & 1;
81
+ for (i = 0; i < 8; i++) v[D.inPort[i]] = (this.inPort >> i) & 1;
82
+
83
+ // The address is the one latched on the previous edge, which is exactly why
84
+ // a load costs two honest cycles.
85
+ const addr = this.field(D.ramAddr);
86
+ const rdata = this.ram[addr];
87
+ for (i = 0; i < 8; i++) v[D.ramRdata[i]] = (rdata >> i) & 1;
88
+
89
+ const g = D.gates;
90
+ let flipped = 0;
91
+ for (let j = 0; j < g.length; j += 3) {
92
+ const y = g[j + 2];
93
+ const val = 1 - (v[g[j]] & v[g[j + 1]]);
94
+ if (v[y] !== val) { v[y] = val; flipped++; }
95
+ }
96
+ this.switched = flipped;
97
+
98
+ const f = D.flops, nx = this.next;
99
+ for (i = 0; i < D.flopCount; i++) nx[i] = v[f[2 * i]];
100
+ for (i = 0; i < D.flopCount; i++) v[f[2 * i + 1]] = nx[i];
101
+
102
+ if (this.flop(D.ramWe) === 1) {
103
+ this.ram[this.field(D.ramAddr)] = this.field(D.ramWdata);
104
+ }
105
+
106
+ this.cycle++;
107
+ return word;
108
+ };
109
+
110
+ /* ----------------------------------------------------------- the assembler
111
+ *
112
+ * The node assembler already exists for the build. Reusing it here means the
113
+ * terminal and the browser workbench cannot accept different source.
114
+ */
115
+
116
+ module.exports = { Machine: Machine, loadNetlist: loadNetlist };