SNAPKITTYWEST commited on
Commit
ae44e8c
·
verified ·
1 Parent(s): c01fd1c

October 2026 main drop: mirror from GitHub (part 2)

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. crates/jxcl-golden/tests/vectors/002_decrement.golden +6 -0
  2. crates/jxcl-hardware-test/Cargo.toml +15 -0
  3. crates/jxcl-hardware-test/README.md +72 -0
  4. crates/jxcl-hardware-test/src/lib.rs +107 -0
  5. crates/jxcl-hardware-test/tests/golden/core_datapath.v +237 -0
  6. crates/jxcl-hardware-test/tests/golden/core_datapath.vhdl +198 -0
  7. crates/jxcl-hardware-test/tests/golden/mux2.netlist.txt +1 -0
  8. crates/jxcl-hardware-test/tests/golden_regression.rs +66 -0
  9. crates/jxcl-hardware-test/tests/isa_conformance.rs +114 -0
  10. crates/jxcl-hardware/Cargo.toml +14 -0
  11. crates/jxcl-hardware/README.md +65 -0
  12. crates/jxcl-hardware/src/lib.rs +196 -0
  13. crates/jxcl-hdl/Cargo.toml +9 -0
  14. crates/jxcl-hdl/README.md +36 -0
  15. crates/jxcl-hdl/src/lib.rs +360 -0
  16. crates/jxcl-heap/Cargo.toml +15 -0
  17. crates/jxcl-heap/README.md +59 -0
  18. crates/jxcl-heap/src/lib.rs +244 -0
  19. crates/jxcl-http/Cargo.toml +15 -0
  20. crates/jxcl-http/README.md +45 -0
  21. crates/jxcl-http/src/lib.rs +137 -0
  22. crates/jxcl-instruction-validation/Cargo.toml +13 -0
  23. crates/jxcl-instruction-validation/README.md +35 -0
  24. crates/jxcl-instruction-validation/src/lib.rs +489 -0
  25. crates/jxcl-instructions/Cargo.toml +13 -0
  26. crates/jxcl-instructions/README.md +88 -0
  27. crates/jxcl-instructions/src/lib.rs +284 -0
  28. crates/jxcl-integration/Cargo.toml +12 -0
  29. crates/jxcl-integration/README.md +39 -0
  30. crates/jxcl-integration/src/lib.rs +23 -0
  31. crates/jxcl-interrupts/Cargo.toml +10 -0
  32. crates/jxcl-interrupts/README.md +37 -0
  33. crates/jxcl-interrupts/src/lib.rs +203 -0
  34. crates/jxcl-isa-metadata/Cargo.toml +10 -0
  35. crates/jxcl-isa-metadata/README.md +41 -0
  36. crates/jxcl-isa-metadata/src/lib.rs +231 -0
  37. crates/jxcl-isa-schema/Cargo.toml +14 -0
  38. crates/jxcl-isa-schema/README.md +41 -0
  39. crates/jxcl-isa-schema/src/lib.rs +306 -0
  40. crates/jxcl-isa-versioning/Cargo.toml +9 -0
  41. crates/jxcl-isa-versioning/README.md +44 -0
  42. crates/jxcl-isa-versioning/src/lib.rs +201 -0
  43. crates/jxcl-lexer/Cargo.toml +9 -0
  44. crates/jxcl-lexer/README.md +36 -0
  45. crates/jxcl-lexer/src/lib.rs +271 -0
  46. crates/jxcl-linker/Cargo.toml +13 -0
  47. crates/jxcl-linker/README.md +40 -0
  48. crates/jxcl-linker/src/lib.rs +335 -0
  49. crates/jxcl-load-store/Cargo.toml +15 -0
  50. crates/jxcl-load-store/README.md +51 -0
crates/jxcl-golden/tests/vectors/002_decrement.golden ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ name: decrement
2
+ program: 02 01 05 00 00 00 00 00 00 00 1A 01 60
3
+ halted: true
4
+ fault: none
5
+ registers: 1=4
6
+ flags: none
crates/jxcl-hardware-test/Cargo.toml ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [package]
2
+ name = "jxcl-hardware-test"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+ description = "Golden-file regression tests for generated Verilog/VHDL/netlist, plus the mechanical width/opcode-count cross-check against the ISA schema."
6
+ license = "AGPL-3.0-only OR LicenseRef-Commercial"
7
+
8
+ [dependencies]
9
+ jxcl-hardware = { path = "../jxcl-hardware" }
10
+ jxcl-isa-schema = { path = "../jxcl-isa-schema" }
11
+ jxcl-hdl = { path = "../jxcl-hdl" }
12
+ jxcl-netlist = { path = "../jxcl-netlist" }
13
+ jxcl-opcodes = { path = "../jxcl-opcodes" }
14
+ jxcl-constants = { path = "../jxcl-constants" }
15
+ jxcl-rtl = { path = "../jxcl-rtl" }
crates/jxcl-hardware-test/README.md ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # jxcl-hardware-test
2
+
3
+ Golden-file regression tests for generated Verilog/VHDL/netlist, plus the mechanical width/opcode-count cross-check against the ISA schema.
4
+
5
+ ## Honesty boundary
6
+
7
+ This is a test-only crate. Nothing in it simulates the generated RTL or
8
+ feeds it to a real Verilog/VHDL/synthesis toolchain -- there is no
9
+ `iverilog`, `verilator`, or `yosys` in this environment (see
10
+ `docs/HARDWARE_LIMITATIONS.md` at the workspace root). "Passes" here
11
+ means: matches a checked-in golden file under `tests/golden/`, or
12
+ matches a structural fact mechanically computed from
13
+ `jxcl-opcodes`/`jxcl-constants`'s real tables/constants -- not "was
14
+ hardware-verified."
15
+
16
+ **`jxcl-isa-schema` status at the time this crate was written:**
17
+ `jxcl-isa-schema` (this crate's other declared dependency) is still a
18
+ placeholder -- no `IsaSchema` type or `generate`/`check_against_spec`
19
+ methods -- as of this writing. `tests/isa_conformance.rs` therefore
20
+ cross-checks the generated decoder directly against
21
+ `jxcl-opcodes::all_defs()` and `jxcl-constants`'s real constants,
22
+ instead of going through `jxcl-isa-schema`. Once `jxcl-isa-schema`
23
+ lands, that test should be switched to compare against
24
+ `IsaSchema::generate()` instead, without changing what it actually
25
+ asserts (a real `IsaSchema` would itself be generated from the same two
26
+ crates).
27
+
28
+ ## Architecture
29
+
30
+ **Owns:** The hardware golden files (`tests/golden/`) and the
31
+ ISA-vs-RTL conformance check.
32
+
33
+ **Category:** hardware · **Source:** new
34
+
35
+ ## Public API
36
+
37
+ Test-only crate; `render_netlist_text` is exposed only so a `Netlist`
38
+ can be golden-file compared the same way rendered Verilog/VHDL text is.
39
+
40
+ ## Dependencies
41
+
42
+ Workspace crates:
43
+
44
+ - `jxcl-hardware`
45
+ - `jxcl-isa-schema` (placeholder as of this writing -- see above)
46
+ - `jxcl-hdl`
47
+ - `jxcl-netlist`
48
+ - `jxcl-opcodes`
49
+ - `jxcl-constants`
50
+ - `jxcl-rtl`
51
+
52
+ External crates:
53
+
54
+ *(none)*
55
+
56
+ ## Testing
57
+
58
+ 10 tests total: 2 unit tests for `render_netlist_text`, 4 golden-file
59
+ regression tests (`tests/golden_regression.rs`, comparing
60
+ `jxcl-hardware`'s emitted Verilog/VHDL and a synthesized netlist against
61
+ checked-in files under `tests/golden/`), and 4 ISA conformance tests
62
+ (`tests/isa_conformance.rs`). Run with `cargo test -p
63
+ jxcl-hardware-test`.
64
+
65
+ ## Status
66
+
67
+ `implemented`, with the `jxcl-isa-schema` cross-check as a documented
68
+ direct-table fallback pending that crate's real implementation.
69
+
70
+ ## License
71
+
72
+ AGPLv3 / Commercial dual-license. See `LICENSE-AGPL`, `LICENSE-COMMERCIAL`, and `LICENSE-NOTICE` at the repository root.
crates/jxcl-hardware-test/src/lib.rs ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Copyright (C) 2026 ERRANT DIGITAL INSTITUTE OF TECHNOLOGY (SEIT NGO) % SNAPKITTY COLLECTIVE
2
+ // SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial
3
+ // See LICENSE-AGPL and LICENSE-COMMERCIAL in this repository for license terms.
4
+
5
+ //! Golden-file regression tests for `jxcl-hardware`'s emitted
6
+ //! Verilog/VHDL/netlist, plus the mechanical width/opcode-count
7
+ //! cross-check against the ISA source of truth.
8
+ //!
9
+ //! # Scope and honesty boundary
10
+ //!
11
+ //! This is a test-only crate: it has no public API of its own beyond
12
+ //! [`render_netlist_text`], a small deterministic text serialization of
13
+ //! a [`jxcl_netlist::Netlist`] used purely so a netlist can be compared
14
+ //! against a checked-in golden file the same way rendered Verilog/VHDL
15
+ //! text is. Nothing in this crate simulates the generated RTL or feeds
16
+ //! it to a real Verilog/VHDL/synthesis toolchain -- there is no
17
+ //! `iverilog`/`verilator`/`yosys` in this environment (see
18
+ //! `docs/HARDWARE_LIMITATIONS.md` at the workspace root). "Passes" here
19
+ //! means: matches a checked-in golden file, or matches a structural fact
20
+ //! mechanically computed from `jxcl-opcodes`/`jxcl-constants`'s real
21
+ //! tables/constants -- not "was hardware-verified."
22
+ //!
23
+ //! ## `jxcl-isa-schema` status at the time this crate was written
24
+ //!
25
+ //! `jxcl-isa-schema` (this crate's other declared dependency, per
26
+ //! `docs/crates.toml`) is still a placeholder -- no `IsaSchema` type, no
27
+ //! `IsaSchema::generate`/`check_against_spec` -- as of this writing. The
28
+ //! `tests/isa_conformance.rs` cross-check in this crate therefore
29
+ //! compares `jxcl-hardware`'s generated decoder directly against
30
+ //! `jxcl-opcodes`/`jxcl-constants`'s real tables/constants, exactly the
31
+ //! way `jxcl-rtl`'s own conformance test does. Once `jxcl-isa-schema`
32
+ //! lands with a real generated schema, that test should be switched to
33
+ //! compare against `IsaSchema::generate()` instead (so this crate is
34
+ //! checking the RTL against the ISA's own schema layer, one level higher
35
+ //! than `jxcl-rtl`'s direct-table check), without changing what it
36
+ //! actually asserts.
37
+ #![forbid(unsafe_code)]
38
+
39
+ use jxcl_netlist::{GateKind, Netlist};
40
+
41
+ /// A small, deterministic text serialization of a [`Netlist`]: one line
42
+ /// per gate, sorted by output name so the result doesn't depend on
43
+ /// construction order, in the form `kind(in1, in2, ...) -> output`.
44
+ ///
45
+ /// This exists only so a [`Netlist`] (which has no inherent canonical
46
+ /// text form of its own -- it's a structural Rust value, not a hardware
47
+ /// description) can be golden-file tested the same way rendered
48
+ /// Verilog/VHDL text already is. It is not a hardware description
49
+ /// format and nothing reads it back.
50
+ pub fn render_netlist_text(netlist: &Netlist) -> String {
51
+ let mut lines: Vec<String> = netlist
52
+ .gates
53
+ .iter()
54
+ .map(|g| {
55
+ let kind = match g.kind {
56
+ GateKind::And => "AND",
57
+ GateKind::Or => "OR",
58
+ GateKind::Not => "NOT",
59
+ GateKind::Mux => "MUX",
60
+ };
61
+ format!("{kind}({}) -> {}", g.inputs.join(", "), g.output)
62
+ })
63
+ .collect();
64
+ lines.sort();
65
+
66
+ let mut wires: Vec<String> = netlist
67
+ .wires
68
+ .iter()
69
+ .map(|w| format!("wire {} [{} bits]", w.name, w.width))
70
+ .collect();
71
+ wires.sort();
72
+
73
+ let mut out = lines.join("\n");
74
+ if !wires.is_empty() {
75
+ if !out.is_empty() {
76
+ out.push('\n');
77
+ }
78
+ out.push_str(&wires.join("\n"));
79
+ }
80
+ out.push('\n');
81
+ out
82
+ }
83
+
84
+ #[cfg(test)]
85
+ mod tests {
86
+ use super::*;
87
+ use jxcl_netlist::{Gate, Wire};
88
+
89
+ #[test]
90
+ fn render_netlist_text_is_sorted_and_deterministic() {
91
+ let net = Netlist::new()
92
+ .with_gates(vec![
93
+ Gate::mux("sel", "a", "b", "z"),
94
+ Gate::not("x", "a_inv"),
95
+ ])
96
+ .with_wires(vec![Wire::new("internal", 4)]);
97
+ let text = render_netlist_text(&net);
98
+ let expected = "MUX(sel, a, b) -> z\nNOT(x) -> a_inv\nwire internal [4 bits]\n";
99
+ assert_eq!(text, expected);
100
+ }
101
+
102
+ #[test]
103
+ fn render_netlist_text_handles_empty_netlist() {
104
+ let net = Netlist::new();
105
+ assert_eq!(render_netlist_text(&net), "\n");
106
+ }
107
+ }
crates/jxcl-hardware-test/tests/golden/core_datapath.v ADDED
@@ -0,0 +1,237 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ module decoder(
2
+ input wire [7:0] opcode,
3
+ output reg valid,
4
+ output reg [3:0] instr_len
5
+ );
6
+
7
+ always @(*) begin
8
+ case (opcode)
9
+ 8'd0: begin
10
+ valid = 1'd1;
11
+ instr_len = 4'd1;
12
+ end
13
+ 8'd1: begin
14
+ valid = 1'd1;
15
+ instr_len = 4'd3;
16
+ end
17
+ 8'd2: begin
18
+ valid = 1'd1;
19
+ instr_len = 4'd10;
20
+ end
21
+ 8'd3: begin
22
+ valid = 1'd1;
23
+ instr_len = 4'd7;
24
+ end
25
+ 8'd4: begin
26
+ valid = 1'd1;
27
+ instr_len = 4'd7;
28
+ end
29
+ 8'd5: begin
30
+ valid = 1'd1;
31
+ instr_len = 4'd2;
32
+ end
33
+ 8'd6: begin
34
+ valid = 1'd1;
35
+ instr_len = 4'd2;
36
+ end
37
+ 8'd7: begin
38
+ valid = 1'd1;
39
+ instr_len = 4'd7;
40
+ end
41
+ 8'd16: begin
42
+ valid = 1'd1;
43
+ instr_len = 4'd3;
44
+ end
45
+ 8'd17: begin
46
+ valid = 1'd1;
47
+ instr_len = 4'd3;
48
+ end
49
+ 8'd18: begin
50
+ valid = 1'd1;
51
+ instr_len = 4'd3;
52
+ end
53
+ 8'd19: begin
54
+ valid = 1'd1;
55
+ instr_len = 4'd3;
56
+ end
57
+ 8'd20: begin
58
+ valid = 1'd1;
59
+ instr_len = 4'd3;
60
+ end
61
+ 8'd21: begin
62
+ valid = 1'd1;
63
+ instr_len = 4'd3;
64
+ end
65
+ 8'd22: begin
66
+ valid = 1'd1;
67
+ instr_len = 4'd3;
68
+ end
69
+ 8'd23: begin
70
+ valid = 1'd1;
71
+ instr_len = 4'd3;
72
+ end
73
+ 8'd24: begin
74
+ valid = 1'd1;
75
+ instr_len = 4'd2;
76
+ end
77
+ 8'd25: begin
78
+ valid = 1'd1;
79
+ instr_len = 4'd2;
80
+ end
81
+ 8'd26: begin
82
+ valid = 1'd1;
83
+ instr_len = 4'd2;
84
+ end
85
+ 8'd32: begin
86
+ valid = 1'd1;
87
+ instr_len = 4'd3;
88
+ end
89
+ 8'd33: begin
90
+ valid = 1'd1;
91
+ instr_len = 4'd3;
92
+ end
93
+ 8'd34: begin
94
+ valid = 1'd1;
95
+ instr_len = 4'd3;
96
+ end
97
+ 8'd35: begin
98
+ valid = 1'd1;
99
+ instr_len = 4'd2;
100
+ end
101
+ 8'd36: begin
102
+ valid = 1'd1;
103
+ instr_len = 4'd3;
104
+ end
105
+ 8'd37: begin
106
+ valid = 1'd1;
107
+ instr_len = 4'd3;
108
+ end
109
+ 8'd38: begin
110
+ valid = 1'd1;
111
+ instr_len = 4'd4;
112
+ end
113
+ 8'd48: begin
114
+ valid = 1'd1;
115
+ instr_len = 4'd3;
116
+ end
117
+ 8'd49: begin
118
+ valid = 1'd1;
119
+ instr_len = 4'd3;
120
+ end
121
+ 8'd50: begin
122
+ valid = 1'd1;
123
+ instr_len = 4'd3;
124
+ end
125
+ 8'd51: begin
126
+ valid = 1'd1;
127
+ instr_len = 4'd3;
128
+ end
129
+ 8'd52: begin
130
+ valid = 1'd1;
131
+ instr_len = 4'd3;
132
+ end
133
+ 8'd64: begin
134
+ valid = 1'd1;
135
+ instr_len = 4'd3;
136
+ end
137
+ 8'd65: begin
138
+ valid = 1'd1;
139
+ instr_len = 4'd3;
140
+ end
141
+ 8'd80: begin
142
+ valid = 1'd1;
143
+ instr_len = 4'd5;
144
+ end
145
+ 8'd81: begin
146
+ valid = 1'd1;
147
+ instr_len = 4'd5;
148
+ end
149
+ 8'd82: begin
150
+ valid = 1'd1;
151
+ instr_len = 4'd1;
152
+ end
153
+ 8'd83: begin
154
+ valid = 1'd1;
155
+ instr_len = 4'd5;
156
+ end
157
+ 8'd84: begin
158
+ valid = 1'd1;
159
+ instr_len = 4'd5;
160
+ end
161
+ 8'd85: begin
162
+ valid = 1'd1;
163
+ instr_len = 4'd5;
164
+ end
165
+ 8'd86: begin
166
+ valid = 1'd1;
167
+ instr_len = 4'd5;
168
+ end
169
+ 8'd87: begin
170
+ valid = 1'd1;
171
+ instr_len = 4'd5;
172
+ end
173
+ 8'd88: begin
174
+ valid = 1'd1;
175
+ instr_len = 4'd5;
176
+ end
177
+ 8'd89: begin
178
+ valid = 1'd1;
179
+ instr_len = 4'd5;
180
+ end
181
+ 8'd90: begin
182
+ valid = 1'd1;
183
+ instr_len = 4'd5;
184
+ end
185
+ 8'd96: begin
186
+ valid = 1'd1;
187
+ instr_len = 4'd1;
188
+ end
189
+ 8'd97: begin
190
+ valid = 1'd1;
191
+ instr_len = 4'd3;
192
+ end
193
+ 8'd98: begin
194
+ valid = 1'd1;
195
+ instr_len = 4'd3;
196
+ end
197
+ 8'd112: begin
198
+ valid = 1'd1;
199
+ instr_len = 4'd8;
200
+ end
201
+ 8'd113: begin
202
+ valid = 1'd1;
203
+ instr_len = 4'd7;
204
+ end
205
+ 8'd114: begin
206
+ valid = 1'd1;
207
+ instr_len = 4'd1;
208
+ end
209
+ default: begin
210
+ valid = 1'd0;
211
+ instr_len = 4'd0;
212
+ end
213
+ endcase
214
+ end
215
+
216
+ endmodule
217
+
218
+ module alu(
219
+ input wire [63:0] op_a,
220
+ input wire [63:0] op_b,
221
+ input wire [7:0] alu_op,
222
+ output wire [63:0] result
223
+ );
224
+
225
+ endmodule
226
+
227
+ module register_file(
228
+ input wire [4:0] read_addr1,
229
+ input wire [4:0] read_addr2,
230
+ output wire [63:0] read_data1,
231
+ output wire [63:0] read_data2,
232
+ input wire [4:0] write_addr,
233
+ input wire [63:0] write_data,
234
+ input wire write_enable
235
+ );
236
+
237
+ endmodule
crates/jxcl-hardware-test/tests/golden/core_datapath.vhdl ADDED
@@ -0,0 +1,198 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ entity decoder is
2
+ port (
3
+ opcode : in std_logic_vector(7 downto 0);
4
+ valid : out std_logic;
5
+ instr_len : out std_logic_vector(3 downto 0)
6
+ );
7
+ end entity decoder;
8
+
9
+ architecture rtl of decoder is
10
+ begin
11
+ process (all) is
12
+ begin
13
+ case opcode is
14
+ when "00000000" =>
15
+ valid <= '1';
16
+ instr_len <= "0001";
17
+ when "00000001" =>
18
+ valid <= '1';
19
+ instr_len <= "0011";
20
+ when "00000010" =>
21
+ valid <= '1';
22
+ instr_len <= "1010";
23
+ when "00000011" =>
24
+ valid <= '1';
25
+ instr_len <= "0111";
26
+ when "00000100" =>
27
+ valid <= '1';
28
+ instr_len <= "0111";
29
+ when "00000101" =>
30
+ valid <= '1';
31
+ instr_len <= "0010";
32
+ when "00000110" =>
33
+ valid <= '1';
34
+ instr_len <= "0010";
35
+ when "00000111" =>
36
+ valid <= '1';
37
+ instr_len <= "0111";
38
+ when "00010000" =>
39
+ valid <= '1';
40
+ instr_len <= "0011";
41
+ when "00010001" =>
42
+ valid <= '1';
43
+ instr_len <= "0011";
44
+ when "00010010" =>
45
+ valid <= '1';
46
+ instr_len <= "0011";
47
+ when "00010011" =>
48
+ valid <= '1';
49
+ instr_len <= "0011";
50
+ when "00010100" =>
51
+ valid <= '1';
52
+ instr_len <= "0011";
53
+ when "00010101" =>
54
+ valid <= '1';
55
+ instr_len <= "0011";
56
+ when "00010110" =>
57
+ valid <= '1';
58
+ instr_len <= "0011";
59
+ when "00010111" =>
60
+ valid <= '1';
61
+ instr_len <= "0011";
62
+ when "00011000" =>
63
+ valid <= '1';
64
+ instr_len <= "0010";
65
+ when "00011001" =>
66
+ valid <= '1';
67
+ instr_len <= "0010";
68
+ when "00011010" =>
69
+ valid <= '1';
70
+ instr_len <= "0010";
71
+ when "00100000" =>
72
+ valid <= '1';
73
+ instr_len <= "0011";
74
+ when "00100001" =>
75
+ valid <= '1';
76
+ instr_len <= "0011";
77
+ when "00100010" =>
78
+ valid <= '1';
79
+ instr_len <= "0011";
80
+ when "00100011" =>
81
+ valid <= '1';
82
+ instr_len <= "0010";
83
+ when "00100100" =>
84
+ valid <= '1';
85
+ instr_len <= "0011";
86
+ when "00100101" =>
87
+ valid <= '1';
88
+ instr_len <= "0011";
89
+ when "00100110" =>
90
+ valid <= '1';
91
+ instr_len <= "0100";
92
+ when "00110000" =>
93
+ valid <= '1';
94
+ instr_len <= "0011";
95
+ when "00110001" =>
96
+ valid <= '1';
97
+ instr_len <= "0011";
98
+ when "00110010" =>
99
+ valid <= '1';
100
+ instr_len <= "0011";
101
+ when "00110011" =>
102
+ valid <= '1';
103
+ instr_len <= "0011";
104
+ when "00110100" =>
105
+ valid <= '1';
106
+ instr_len <= "0011";
107
+ when "01000000" =>
108
+ valid <= '1';
109
+ instr_len <= "0011";
110
+ when "01000001" =>
111
+ valid <= '1';
112
+ instr_len <= "0011";
113
+ when "01010000" =>
114
+ valid <= '1';
115
+ instr_len <= "0101";
116
+ when "01010001" =>
117
+ valid <= '1';
118
+ instr_len <= "0101";
119
+ when "01010010" =>
120
+ valid <= '1';
121
+ instr_len <= "0001";
122
+ when "01010011" =>
123
+ valid <= '1';
124
+ instr_len <= "0101";
125
+ when "01010100" =>
126
+ valid <= '1';
127
+ instr_len <= "0101";
128
+ when "01010101" =>
129
+ valid <= '1';
130
+ instr_len <= "0101";
131
+ when "01010110" =>
132
+ valid <= '1';
133
+ instr_len <= "0101";
134
+ when "01010111" =>
135
+ valid <= '1';
136
+ instr_len <= "0101";
137
+ when "01011000" =>
138
+ valid <= '1';
139
+ instr_len <= "0101";
140
+ when "01011001" =>
141
+ valid <= '1';
142
+ instr_len <= "0101";
143
+ when "01011010" =>
144
+ valid <= '1';
145
+ instr_len <= "0101";
146
+ when "01100000" =>
147
+ valid <= '1';
148
+ instr_len <= "0001";
149
+ when "01100001" =>
150
+ valid <= '1';
151
+ instr_len <= "0011";
152
+ when "01100010" =>
153
+ valid <= '1';
154
+ instr_len <= "0011";
155
+ when "01110000" =>
156
+ valid <= '1';
157
+ instr_len <= "1000";
158
+ when "01110001" =>
159
+ valid <= '1';
160
+ instr_len <= "0111";
161
+ when "01110010" =>
162
+ valid <= '1';
163
+ instr_len <= "0001";
164
+ when others =>
165
+ valid <= '0';
166
+ instr_len <= "0000";
167
+ end case;
168
+ end process;
169
+ end architecture rtl;
170
+
171
+ entity alu is
172
+ port (
173
+ op_a : in std_logic_vector(63 downto 0);
174
+ op_b : in std_logic_vector(63 downto 0);
175
+ alu_op : in std_logic_vector(7 downto 0);
176
+ result : out std_logic_vector(63 downto 0)
177
+ );
178
+ end entity alu;
179
+
180
+ architecture rtl of alu is
181
+ begin
182
+ end architecture rtl;
183
+
184
+ entity register_file is
185
+ port (
186
+ read_addr1 : in std_logic_vector(4 downto 0);
187
+ read_addr2 : in std_logic_vector(4 downto 0);
188
+ read_data1 : out std_logic_vector(63 downto 0);
189
+ read_data2 : out std_logic_vector(63 downto 0);
190
+ write_addr : in std_logic_vector(4 downto 0);
191
+ write_data : in std_logic_vector(63 downto 0);
192
+ write_enable : in std_logic
193
+ );
194
+ end entity register_file;
195
+
196
+ architecture rtl of register_file is
197
+ begin
198
+ end architecture rtl;
crates/jxcl-hardware-test/tests/golden/mux2.netlist.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ MUX(sel, a, b) -> y
crates/jxcl-hardware-test/tests/golden_regression.rs ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Copyright (C) 2026 ERRANT DIGITAL INSTITUTE OF TECHNOLOGY (SEIT NGO) % SNAPKITTY COLLECTIVE
2
+ // SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial
3
+ // See LICENSE-AGPL and LICENSE-COMMERCIAL in this repository for license terms.
4
+
5
+ //! Golden-file regression tests for `jxcl-hardware`'s emitted
6
+ //! Verilog/VHDL text and the toy synthesis pass's netlist output.
7
+ //!
8
+ //! See `jxcl-hardware-test`'s crate docs for the honesty boundary: a
9
+ //! pass here means "matches the checked-in golden file," never "was
10
+ //! simulated or synthesized by a real toolchain" (none is available in
11
+ //! this environment -- `docs/HARDWARE_LIMITATIONS.md`).
12
+
13
+ #[test]
14
+ fn emitted_verilog_matches_golden_file() {
15
+ let text = jxcl_hardware::emit_verilog();
16
+ let expected = include_str!("golden/core_datapath.v");
17
+ assert_eq!(
18
+ text, expected,
19
+ "jxcl_hardware::emit_verilog() drifted from tests/golden/core_datapath.v -- \
20
+ if this is an intentional RTL change, regenerate the golden file"
21
+ );
22
+ }
23
+
24
+ #[test]
25
+ fn emitted_vhdl_matches_golden_file() {
26
+ let text = jxcl_hardware::emit_vhdl();
27
+ let expected = include_str!("golden/core_datapath.vhdl");
28
+ assert_eq!(
29
+ text, expected,
30
+ "jxcl_hardware::emit_vhdl() drifted from tests/golden/core_datapath.vhdl -- \
31
+ if this is an intentional RTL change, regenerate the golden file"
32
+ );
33
+ }
34
+
35
+ #[test]
36
+ fn emitted_mux2_netlist_matches_golden_file() {
37
+ // The canonical mux2 example is the one module that actually falls
38
+ // inside jxcl-synthesis's narrow toy scope (see jxcl-hardware's
39
+ // NetlistReport docs for why none of the three real core datapath
40
+ // modules do, today).
41
+ let netlist = jxcl_hardware::emit_netlist_for(&jxcl_hdl::examples::mux2_module())
42
+ .expect("mux2 is in the toy synthesis pass's documented scope");
43
+ netlist
44
+ .validate()
45
+ .expect("synthesized netlist must be structurally well-formed");
46
+
47
+ let text = jxcl_hardware_test::render_netlist_text(&netlist);
48
+ let expected = include_str!("golden/mux2.netlist.txt");
49
+ assert_eq!(
50
+ text, expected,
51
+ "synthesized mux2 netlist drifted from tests/golden/mux2.netlist.txt"
52
+ );
53
+ }
54
+
55
+ #[test]
56
+ fn core_datapath_netlist_report_is_still_all_out_of_scope() {
57
+ // A regression guard on the honesty claim itself: if a future
58
+ // change to jxcl-rtl's generated modules or jxcl-synthesis's scope
59
+ // ever makes one of these actually synthesizable, this test will
60
+ // fail here (loudly, in a test named for exactly that) rather than
61
+ // silently starting to under-report scope.
62
+ let report = jxcl_hardware::emit_netlist();
63
+ assert!(report.decoder.is_err());
64
+ assert!(report.alu.is_err());
65
+ assert!(report.register_file.is_err());
66
+ }
crates/jxcl-hardware-test/tests/isa_conformance.rs ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Copyright (C) 2026 ERRANT DIGITAL INSTITUTE OF TECHNOLOGY (SEIT NGO) % SNAPKITTY COLLECTIVE
2
+ // SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial
3
+ // See LICENSE-AGPL and LICENSE-COMMERCIAL in this repository for license terms.
4
+
5
+ //! The mechanical width/opcode-count cross-check between
6
+ //! `jxcl-hardware`'s generated decoder and the real ISA source of
7
+ //! truth (`jxcl-opcodes`/`jxcl-constants`).
8
+ //!
9
+ //! # `jxcl-isa-schema` status
10
+ //!
11
+ //! `docs/crates.toml` calls for this cross-check to go through
12
+ //! `jxcl-isa-schema`. That crate is still a placeholder (no
13
+ //! `IsaSchema` type) as of this writing, so this test cross-checks
14
+ //! directly against `jxcl-opcodes::all_defs()` and
15
+ //! `jxcl-constants`'s real constants instead -- the same real source of
16
+ //! truth `jxcl-isa-schema` would itself be generated from. Once
17
+ //! `jxcl-isa-schema` lands, this test's assertions should be rewritten
18
+ //! against `jxcl_isa_schema::IsaSchema::generate()` in place of the
19
+ //! direct `jxcl-opcodes`/`jxcl-constants` calls below, without changing
20
+ //! what they actually assert (the numbers are the same either way,
21
+ //! since a real `IsaSchema` would itself be generated from these same
22
+ //! two crates).
23
+
24
+ use jxcl_hdl::{CaseValue, Statement};
25
+ use std::collections::BTreeSet;
26
+
27
+ fn decoder_arms() -> Vec<(CaseValue, Vec<Statement>)> {
28
+ let module = jxcl_rtl::build_decoder_module();
29
+ match module
30
+ .top_level_case()
31
+ .expect("decoder module must have a top-level case statement")
32
+ {
33
+ Statement::Case { arms, .. } => arms.clone(),
34
+ _ => unreachable!(),
35
+ }
36
+ }
37
+
38
+ /// The headline mechanical proof this crate exists to carry (per
39
+ /// `docs/RTL_CONTRACT.md` and rule #14): the number of real, non-default
40
+ /// case arms in the generated decoder equals the number of real entries
41
+ /// in `jxcl_opcodes::all_defs()`, one for one, by opcode id -- checked
42
+ /// again here at the `jxcl-hardware-test` level (on top of `jxcl-rtl`'s
43
+ /// own equivalent test), so a regression in either crate is caught by
44
+ /// at least one of the two.
45
+ #[test]
46
+ fn decoder_opcode_arm_count_matches_jxcl_opcodes_table() {
47
+ let arms = decoder_arms();
48
+ let generated_ids: BTreeSet<u64> = arms
49
+ .iter()
50
+ .filter_map(|(v, _)| match v {
51
+ CaseValue::Literal { value, .. } => Some(*value),
52
+ CaseValue::Default => None,
53
+ })
54
+ .collect();
55
+ let real_ids: BTreeSet<u64> = jxcl_opcodes::all_defs()
56
+ .iter()
57
+ .map(|d| d.opcode as u64)
58
+ .collect();
59
+
60
+ assert_eq!(generated_ids.len(), jxcl_opcodes::all_defs().len());
61
+ assert_eq!(generated_ids, real_ids);
62
+ }
63
+
64
+ /// The decoder's `opcode` input port width must equal
65
+ /// `jxcl_constants::OPCODE_BITS` exactly -- if the ISA's opcode field
66
+ /// ever widened or narrowed without regenerating the RTL, this fails.
67
+ #[test]
68
+ fn decoder_opcode_field_width_matches_jxcl_constants() {
69
+ let module = jxcl_rtl::build_decoder_module();
70
+ let opcode_port = module
71
+ .ports
72
+ .iter()
73
+ .find(|p| p.name == "opcode")
74
+ .expect("decoder has an opcode port");
75
+ assert_eq!(opcode_port.width, jxcl_constants::OPCODE_BITS);
76
+ }
77
+
78
+ /// Every opcode actually fits in the opcode space `jxcl_constants`
79
+ /// declares -- a sanity bound shared with `jxcl-opcodes`'s own test of
80
+ /// the same fact, re-checked here at the RTL-generation boundary.
81
+ #[test]
82
+ fn every_real_opcode_fits_the_declared_opcode_space() {
83
+ for def in jxcl_opcodes::all_defs() {
84
+ assert!((def.opcode as usize) < jxcl_constants::OPCODE_SPACE);
85
+ }
86
+ }
87
+
88
+ /// The register file's address port width must be wide enough to name
89
+ /// every real register in `jxcl_constants::REGISTER_COUNT`, and its data
90
+ /// port widths must equal `jxcl_constants::WORD_BITS` exactly.
91
+ #[test]
92
+ fn register_file_widths_match_jxcl_constants() {
93
+ let module = jxcl_rtl::build_register_file_module();
94
+ let addr_port = module
95
+ .ports
96
+ .iter()
97
+ .find(|p| p.name == "read_addr1")
98
+ .expect("register file has a read_addr1 port");
99
+ let addressable = 1usize << addr_port.width;
100
+ assert!(
101
+ addressable >= jxcl_constants::REGISTER_COUNT,
102
+ "register file address port ({} bits, addresses {} entries) can't name all {} real registers",
103
+ addr_port.width,
104
+ addressable,
105
+ jxcl_constants::REGISTER_COUNT
106
+ );
107
+
108
+ let data_port = module
109
+ .ports
110
+ .iter()
111
+ .find(|p| p.name == "write_data")
112
+ .expect("register file has a write_data port");
113
+ assert_eq!(data_port.width, jxcl_constants::WORD_BITS);
114
+ }
crates/jxcl-hardware/Cargo.toml ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [package]
2
+ name = "jxcl-hardware"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+ description = "Top-level facade integrating the HDL AST, both text backends, and the toy synthesis pass behind one emit API."
6
+ license = "AGPL-3.0-only OR LicenseRef-Commercial"
7
+
8
+ [dependencies]
9
+ jxcl-hdl = { path = "../jxcl-hdl" }
10
+ jxcl-rtl = { path = "../jxcl-rtl" }
11
+ jxcl-verilog = { path = "../jxcl-verilog" }
12
+ jxcl-vhdl = { path = "../jxcl-vhdl" }
13
+ jxcl-netlist = { path = "../jxcl-netlist" }
14
+ jxcl-synthesis = { path = "../jxcl-synthesis" }
crates/jxcl-hardware/README.md ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # jxcl-hardware
2
+
3
+ Top-level facade integrating the HDL AST, both text backends, and the toy synthesis pass behind one emit API.
4
+
5
+ ## Honesty boundary
6
+
7
+ Composing real code does not make the result more verified than its
8
+ parts. Nothing in this crate simulates the emitted Verilog/VHDL, and
9
+ nothing feeds it to a real logic synthesizer -- there is no `iverilog`,
10
+ `verilator`, or `yosys` in this environment (see
11
+ `docs/HARDWARE_LIMITATIONS.md` at the workspace root).
12
+
13
+ `emit_netlist` is explicit about scope: `jxcl-synthesis`'s toy pass only
14
+ handles a narrow 2:1-mux-shaped case statement, and **none of the three
15
+ real core datapath modules generated by `jxcl-rtl` currently fit that
16
+ shape** (the decoder has one arm per real opcode, dozens rather than
17
+ two; the ALU/register-file modules carry no top-level case statement at
18
+ all yet). `NetlistReport`'s three fields are therefore all legitimately
19
+ `Err` today, and a test (`netlist_report_honestly_reports_out_of_scope_modules`
20
+ in this crate, and its `jxcl-hardware-test` counterpart) checks that
21
+ fact mechanically rather than leaving it as an unverified doc claim.
22
+ `emit_netlist_for` is provided so a caller can still run the toy pass on
23
+ a module that *does* fit its scope (e.g. `jxcl-hdl`'s canonical `mux2`
24
+ example).
25
+
26
+ ## Architecture
27
+
28
+ **Owns:** `emit_verilog`/`emit_vhdl`/`emit_netlist` entry points for the
29
+ jxcl core datapath.
30
+
31
+ **Category:** hardware · **Source:** new
32
+
33
+ ## Public API
34
+
35
+ `emit_verilog`, `emit_vhdl`, `emit_netlist`, `emit_netlist_for`,
36
+ `CoreDatapath`, `NetlistReport`
37
+
38
+ ## Dependencies
39
+
40
+ Workspace crates:
41
+
42
+ - `jxcl-hdl`
43
+ - `jxcl-rtl`
44
+ - `jxcl-verilog`
45
+ - `jxcl-vhdl`
46
+ - `jxcl-netlist`
47
+ - `jxcl-synthesis`
48
+
49
+ External crates:
50
+
51
+ *(none)*
52
+
53
+ ## Testing
54
+
55
+ 6 integration-style unit tests covering all three emit functions and
56
+ the honest out-of-scope netlist report. Run with `cargo test -p
57
+ jxcl-hardware`.
58
+
59
+ ## Status
60
+
61
+ `implemented`.
62
+
63
+ ## License
64
+
65
+ AGPLv3 / Commercial dual-license. See `LICENSE-AGPL`, `LICENSE-COMMERCIAL`, and `LICENSE-NOTICE` at the repository root.
crates/jxcl-hardware/src/lib.rs ADDED
@@ -0,0 +1,196 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Copyright (C) 2026 ERRANT DIGITAL INSTITUTE OF TECHNOLOGY (SEIT NGO) % SNAPKITTY COLLECTIVE
2
+ // SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial
3
+ // See LICENSE-AGPL and LICENSE-COMMERCIAL in this repository for license terms.
4
+
5
+ //! Top-level facade integrating `jxcl-rtl`'s generated core-datapath
6
+ //! modules with both text backends (`jxcl-verilog`, `jxcl-vhdl`) and the
7
+ //! toy structural synthesis pass (`jxcl-synthesis`/`jxcl-netlist`)
8
+ //! behind one `emit_verilog`/`emit_vhdl`/`emit_netlist` API.
9
+ //!
10
+ //! # Scope and honesty boundary
11
+ //!
12
+ //! Composing real code does not make the result more verified than its
13
+ //! parts: nothing in this crate simulates the emitted Verilog/VHDL, and
14
+ //! nothing feeds it to a real logic synthesizer. There is no
15
+ //! `iverilog`/`verilator`/`yosys` in this environment (see
16
+ //! `docs/HARDWARE_LIMITATIONS.md` at the workspace root). See each
17
+ //! function's docs below for exactly what it does and does not cover --
18
+ //! in particular, [`emit_netlist`] is explicit about which of the three
19
+ //! core datapath modules the (intentionally narrow) toy synthesis pass
20
+ //! can and cannot handle today.
21
+ #![forbid(unsafe_code)]
22
+
23
+ use jxcl_netlist::Netlist;
24
+ use jxcl_synthesis::SynthesisError;
25
+
26
+ /// The three `jxcl-hdl` modules that make up jxcl's core datapath, as
27
+ /// generated directly from `jxcl-rtl` (which in turn generates them
28
+ /// from the real `jxcl-opcodes`/`jxcl-constants` tables -- see
29
+ /// `jxcl-rtl`'s own docs for the `jxcl-alu` placeholder caveat on
30
+ /// [`CoreDatapath::alu`]).
31
+ pub struct CoreDatapath {
32
+ pub decoder: jxcl_hdl::Module,
33
+ pub alu: jxcl_hdl::Module,
34
+ pub register_file: jxcl_hdl::Module,
35
+ }
36
+
37
+ impl CoreDatapath {
38
+ /// Generate a fresh copy of all three core datapath modules from
39
+ /// `jxcl-rtl`.
40
+ pub fn generate() -> Self {
41
+ CoreDatapath {
42
+ decoder: jxcl_rtl::build_decoder_module(),
43
+ alu: jxcl_rtl::build_alu_module(),
44
+ register_file: jxcl_rtl::build_register_file_module(),
45
+ }
46
+ }
47
+
48
+ fn modules(&self) -> [(&'static str, &jxcl_hdl::Module); 3] {
49
+ [
50
+ ("decoder", &self.decoder),
51
+ ("alu", &self.alu),
52
+ ("register_file", &self.register_file),
53
+ ]
54
+ }
55
+ }
56
+
57
+ impl Default for CoreDatapath {
58
+ fn default() -> Self {
59
+ Self::generate()
60
+ }
61
+ }
62
+
63
+ /// Render all three core datapath modules to Verilog-2001 text (one
64
+ /// module per source module, concatenated in decoder/alu/register_file
65
+ /// order, separated by a blank line). See `jxcl-verilog`'s own docs for
66
+ /// exactly what "renders to Verilog" does and doesn't guarantee (text
67
+ /// that follows Verilog-2001 grammar for this AST's constructs; never
68
+ /// run through a real toolchain).
69
+ pub fn emit_verilog() -> String {
70
+ let datapath = CoreDatapath::generate();
71
+ datapath
72
+ .modules()
73
+ .iter()
74
+ .map(|(_, m)| jxcl_verilog::render_verilog(m))
75
+ .collect::<Vec<_>>()
76
+ .join("\n")
77
+ }
78
+
79
+ /// Render all three core datapath modules to VHDL text, in the same
80
+ /// order and with the same honesty boundary as [`emit_verilog`] (see
81
+ /// `jxcl-vhdl`'s own docs).
82
+ pub fn emit_vhdl() -> String {
83
+ let datapath = CoreDatapath::generate();
84
+ datapath
85
+ .modules()
86
+ .iter()
87
+ .map(|(_, m)| jxcl_vhdl::render_vhdl(m))
88
+ .collect::<Vec<_>>()
89
+ .join("\n")
90
+ }
91
+
92
+ /// Per-module result of running `jxcl-synthesis`'s toy synthesis pass
93
+ /// over each of the three core datapath modules.
94
+ ///
95
+ /// `jxcl-synthesis` is deliberately scoped to a single narrow shape (a
96
+ /// 1-bit-selector, 2-arm, identifier-only case statement -- see its own
97
+ /// docs). **None of the three real core datapath modules currently fit
98
+ /// that shape**: the decoder's case statement has one arm per real
99
+ /// opcode (dozens, not two) and its arm bodies assign literals rather
100
+ /// than bare identifiers, and the ALU/register-file modules (the latter
101
+ /// especially, and the former as a documented `jxcl-alu` placeholder --
102
+ /// see `jxcl-rtl`'s docs) currently carry no top-level case statement at
103
+ /// all. So today, every field here is legitimately `Err`, and that is
104
+ /// the honest, correct answer, not a bug: it says exactly what
105
+ /// `docs/HARDWARE_LIMITATIONS.md` says the toy synthesis pass is scoped
106
+ /// to, applied to what `jxcl-rtl` actually generates today. This struct
107
+ /// exists so that claim is a checked fact (see this crate's
108
+ /// `netlist_report_honestly_reports_out_of_scope_modules` test) rather
109
+ /// than an unverified assertion in a doc comment, and so it stays
110
+ /// correct automatically if a future, richer `jxcl-rtl` module *does*
111
+ /// fall in scope.
112
+ pub struct NetlistReport {
113
+ pub decoder: Result<Netlist, SynthesisError>,
114
+ pub alu: Result<Netlist, SynthesisError>,
115
+ pub register_file: Result<Netlist, SynthesisError>,
116
+ }
117
+
118
+ /// Run `jxcl-synthesis::synthesize` over each of the three core
119
+ /// datapath modules and report the per-module result. See
120
+ /// [`NetlistReport`] for why every field is expected to be `Err` today.
121
+ pub fn emit_netlist() -> NetlistReport {
122
+ let datapath = CoreDatapath::generate();
123
+ NetlistReport {
124
+ decoder: jxcl_synthesis::synthesize(&datapath.decoder),
125
+ alu: jxcl_synthesis::synthesize(&datapath.alu),
126
+ register_file: jxcl_synthesis::synthesize(&datapath.register_file),
127
+ }
128
+ }
129
+
130
+ /// Synthesize an arbitrary `jxcl-hdl` module directly, for a caller that
131
+ /// wants to run the toy synthesis pass on something other than the
132
+ /// three standard core datapath modules (e.g. `jxcl-hdl`'s own `mux2`
133
+ /// canonical example, which *does* fit the pass's scope).
134
+ pub fn emit_netlist_for(module: &jxcl_hdl::Module) -> Result<Netlist, SynthesisError> {
135
+ jxcl_synthesis::synthesize(module)
136
+ }
137
+
138
+ #[cfg(test)]
139
+ mod tests {
140
+ use super::*;
141
+
142
+ #[test]
143
+ fn emit_verilog_contains_all_three_module_headers() {
144
+ let text = emit_verilog();
145
+ assert!(text.contains("module decoder("));
146
+ assert!(text.contains("module alu("));
147
+ assert!(text.contains("module register_file("));
148
+ }
149
+
150
+ #[test]
151
+ fn emit_vhdl_contains_all_three_entity_headers() {
152
+ let text = emit_vhdl();
153
+ assert!(text.contains("entity decoder is"));
154
+ assert!(text.contains("entity alu is"));
155
+ assert!(text.contains("entity register_file is"));
156
+ }
157
+
158
+ #[test]
159
+ fn emitted_verilog_is_balanced() {
160
+ let text = emit_verilog();
161
+ assert_eq!(
162
+ text.matches("module ").count(),
163
+ text.matches("endmodule").count()
164
+ );
165
+ }
166
+
167
+ #[test]
168
+ fn netlist_report_honestly_reports_out_of_scope_modules() {
169
+ // Documents, as a checked fact rather than only a doc comment,
170
+ // that none of the three real generated core-datapath modules
171
+ // fall inside the toy synthesis pass's narrow scope today.
172
+ let report = emit_netlist();
173
+ assert!(report.decoder.is_err());
174
+ assert!(report.alu.is_err());
175
+ assert!(report.register_file.is_err());
176
+ }
177
+
178
+ #[test]
179
+ fn emit_netlist_for_succeeds_on_a_module_that_fits_the_toy_scope() {
180
+ // The canonical mux2 example is exactly the shape jxcl-synthesis
181
+ // supports, proving the wiring (jxcl-hardware -> jxcl-synthesis
182
+ // -> jxcl-netlist) works end to end for an in-scope module.
183
+ let netlist = emit_netlist_for(&jxcl_hdl::examples::mux2_module())
184
+ .expect("mux2 is in the toy synthesis pass's scope");
185
+ assert_eq!(netlist.gates.len(), 1);
186
+ netlist.validate().expect("well-formed netlist");
187
+ }
188
+
189
+ #[test]
190
+ fn core_datapath_generate_matches_jxcl_rtl_directly() {
191
+ let datapath = CoreDatapath::generate();
192
+ assert_eq!(datapath.decoder.name, "decoder");
193
+ assert_eq!(datapath.alu.name, "alu");
194
+ assert_eq!(datapath.register_file.name, "register_file");
195
+ }
196
+ }
crates/jxcl-hdl/Cargo.toml ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ [package]
2
+ name = "jxcl-hdl"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+ description = "A backend-agnostic hardware-description intermediate representation: modules, ports, signals, always-blocks, case-statements."
6
+ license = "AGPL-3.0-only OR LicenseRef-Commercial"
7
+
8
+ [dependencies]
9
+ jxcl-types = { path = "../jxcl-types" }
crates/jxcl-hdl/README.md ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # jxcl-hdl
2
+
3
+ A backend-agnostic hardware-description intermediate representation: modules, ports, signals, always-blocks, case-statements.
4
+
5
+ ## Architecture
6
+
7
+ **Owns:** The HDL AST types.
8
+
9
+ **Category:** hardware · **Source:** new
10
+
11
+ ## Public API
12
+
13
+ `Module`, `Port`, `Signal`, `Statement`
14
+
15
+ ## Dependencies
16
+
17
+ Workspace crates:
18
+
19
+ - `jxcl-types`
20
+
21
+ External crates:
22
+
23
+ *(none)*
24
+
25
+ ## Testing
26
+
27
+ Planned test kinds: unit.
28
+
29
+ ## Status
30
+
31
+ `planned` in `docs/crates.toml` -- see
32
+ `docs/CRATE_GENERATION_PLAN.md` for the implementation batch schedule.
33
+
34
+ ## License
35
+
36
+ AGPLv3 / Commercial dual-license. See `LICENSE-AGPL`, `LICENSE-COMMERCIAL`, and `LICENSE-NOTICE` at the repository root.
crates/jxcl-hdl/src/lib.rs ADDED
@@ -0,0 +1,360 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Copyright (C) 2026 ERRANT DIGITAL INSTITUTE OF TECHNOLOGY (SEIT NGO) % SNAPKITTY COLLECTIVE
2
+ // SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial
3
+ // See LICENSE-AGPL and LICENSE-COMMERCIAL in this repository for license terms.
4
+
5
+ //! A backend-agnostic hardware-description intermediate representation:
6
+ //! modules, ports, signals, always-blocks, case-statements.
7
+ //!
8
+ //! # Scope and honesty boundary
9
+ //!
10
+ //! This crate defines a Rust **AST** for a small, explicitly scoped
11
+ //! subset of RTL-style hardware description. It does not simulate
12
+ //! anything and it does not talk to any external EDA tool. See
13
+ //! `docs/HARDWARE_LIMITATIONS.md` at the workspace root for the full
14
+ //! statement of what the Hardware/RTL crate batch does and does not do
15
+ //! (no `iverilog`/`verilator`/`yosys` is available in this environment,
16
+ //! or assumed to be available by anything in this crate).
17
+ //!
18
+ //! `jxcl-verilog` and `jxcl-vhdl` render this same [`Module`] AST to two
19
+ //! different text backends, which is the concrete proof that the AST
20
+ //! itself is backend-agnostic. `jxcl-synthesis` lowers a small subset of
21
+ //! it to a structural gate netlist ([`jxcl-netlist`]).
22
+ #![forbid(unsafe_code)]
23
+
24
+ /// Port direction, as seen from outside the module.
25
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
26
+ pub enum Direction {
27
+ In,
28
+ Out,
29
+ }
30
+
31
+ /// A module port: a named, directional, fixed-width signal that is part
32
+ /// of the module's external interface.
33
+ #[derive(Debug, Clone, PartialEq, Eq, Hash)]
34
+ pub struct Port {
35
+ pub name: String,
36
+ pub direction: Direction,
37
+ /// Width in bits. `1` is a scalar (`wire`/`std_logic`); anything
38
+ /// greater renders as a vector (`[width-1:0]` / `std_logic_vector`).
39
+ pub width: u32,
40
+ }
41
+
42
+ impl Port {
43
+ pub fn new(name: impl Into<String>, direction: Direction, width: u32) -> Self {
44
+ Port {
45
+ name: name.into(),
46
+ direction,
47
+ width,
48
+ }
49
+ }
50
+
51
+ pub fn input(name: impl Into<String>, width: u32) -> Self {
52
+ Port::new(name, Direction::In, width)
53
+ }
54
+
55
+ pub fn output(name: impl Into<String>, width: u32) -> Self {
56
+ Port::new(name, Direction::Out, width)
57
+ }
58
+ }
59
+
60
+ /// An internal signal (a `wire`/`reg` in Verilog terms, a `signal` in
61
+ /// VHDL terms) that is not part of the module's port list.
62
+ #[derive(Debug, Clone, PartialEq, Eq, Hash)]
63
+ pub struct Signal {
64
+ pub name: String,
65
+ pub width: u32,
66
+ }
67
+
68
+ impl Signal {
69
+ pub fn new(name: impl Into<String>, width: u32) -> Self {
70
+ Signal {
71
+ name: name.into(),
72
+ width,
73
+ }
74
+ }
75
+ }
76
+
77
+ /// An expression that can appear on the right-hand side of an
78
+ /// assignment, or as a `case`/`always` selector.
79
+ ///
80
+ /// This is intentionally a small subset: identifiers, fixed-width
81
+ /// literals, and a fixed-range bit slice of another expression. It is
82
+ /// enough to express opcode-field extraction, register-file addressing,
83
+ /// and simple combinational selection, which is all this crate's
84
+ /// consumers (`jxcl-rtl`) need.
85
+ #[derive(Debug, Clone, PartialEq, Eq, Hash)]
86
+ pub enum Expr {
87
+ /// A reference to a port or signal by name.
88
+ Ident(String),
89
+ /// A fixed-width numeric literal, e.g. the Verilog `8'h10` /
90
+ /// VHDL `"00010000"`.
91
+ Literal { width: u32, value: u64 },
92
+ /// `base[high:low]`, inclusive on both ends, `high >= low`.
93
+ BitSlice {
94
+ base: Box<Expr>,
95
+ high: u32,
96
+ low: u32,
97
+ },
98
+ }
99
+
100
+ impl Expr {
101
+ pub fn ident(name: impl Into<String>) -> Self {
102
+ Expr::Ident(name.into())
103
+ }
104
+
105
+ pub fn literal(width: u32, value: u64) -> Self {
106
+ Expr::Literal { width, value }
107
+ }
108
+
109
+ pub fn bit_slice(base: Expr, high: u32, low: u32) -> Self {
110
+ Expr::BitSlice {
111
+ base: Box::new(base),
112
+ high,
113
+ low,
114
+ }
115
+ }
116
+ }
117
+
118
+ /// The selector value matched by one arm of a [`Statement::Case`].
119
+ #[derive(Debug, Clone, PartialEq, Eq, Hash)]
120
+ pub enum CaseValue {
121
+ /// A specific fixed-width value (Verilog `8'h10:`, VHDL
122
+ /// `when "00010000" =>`).
123
+ Literal { width: u32, value: u64 },
124
+ /// The catch-all arm (Verilog `default:`, VHDL `when others =>`).
125
+ Default,
126
+ }
127
+
128
+ impl CaseValue {
129
+ pub fn literal(width: u32, value: u64) -> Self {
130
+ CaseValue::Literal { width, value }
131
+ }
132
+ }
133
+
134
+ /// The sensitivity of an `always`/`process` block.
135
+ ///
136
+ /// Only combinational sensitivity (`always @(*)` / `process(...)` with
137
+ /// every read signal listed) is modeled; this crate's consumers only
138
+ /// need to describe combinational decode/select logic, not clocked
139
+ /// sequential logic.
140
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
141
+ pub enum Sensitivity {
142
+ Combinational,
143
+ }
144
+
145
+ /// One statement inside a module body or a process/case-arm body.
146
+ #[derive(Debug, Clone, PartialEq, Eq)]
147
+ pub enum Statement {
148
+ /// A (continuous or procedural, depending on context) assignment:
149
+ /// `target = expr;`.
150
+ Assign { target: String, expr: Expr },
151
+ /// A combinational process block: `always @(*) begin ... end` /
152
+ /// `process(...) begin ... end process;`.
153
+ Always {
154
+ sensitivity: Sensitivity,
155
+ body: Vec<Statement>,
156
+ },
157
+ /// A `case`/`case...is` statement: one arm per matched selector
158
+ /// value, each arm holding its own statement body.
159
+ Case {
160
+ selector: Expr,
161
+ arms: Vec<(CaseValue, Vec<Statement>)>,
162
+ },
163
+ }
164
+
165
+ impl Statement {
166
+ pub fn assign(target: impl Into<String>, expr: Expr) -> Self {
167
+ Statement::Assign {
168
+ target: target.into(),
169
+ expr,
170
+ }
171
+ }
172
+
173
+ pub fn always(sensitivity: Sensitivity, body: Vec<Statement>) -> Self {
174
+ Statement::Always { sensitivity, body }
175
+ }
176
+
177
+ pub fn case_stmt(selector: Expr, arms: Vec<(CaseValue, Vec<Statement>)>) -> Self {
178
+ Statement::Case { selector, arms }
179
+ }
180
+ }
181
+
182
+ /// A complete hardware module: a name, an interface (ports), internal
183
+ /// state (signals), and behavior (statements).
184
+ #[derive(Debug, Clone, PartialEq, Eq)]
185
+ pub struct Module {
186
+ pub name: String,
187
+ pub ports: Vec<Port>,
188
+ pub signals: Vec<Signal>,
189
+ pub statements: Vec<Statement>,
190
+ }
191
+
192
+ impl Module {
193
+ pub fn new(name: impl Into<String>) -> Self {
194
+ Module {
195
+ name: name.into(),
196
+ ports: Vec::new(),
197
+ signals: Vec::new(),
198
+ statements: Vec::new(),
199
+ }
200
+ }
201
+
202
+ pub fn with_ports(mut self, ports: Vec<Port>) -> Self {
203
+ self.ports = ports;
204
+ self
205
+ }
206
+
207
+ pub fn with_signals(mut self, signals: Vec<Signal>) -> Self {
208
+ self.signals = signals;
209
+ self
210
+ }
211
+
212
+ pub fn with_statements(mut self, statements: Vec<Statement>) -> Self {
213
+ self.statements = statements;
214
+ self
215
+ }
216
+
217
+ pub fn inputs(&self) -> impl Iterator<Item = &Port> {
218
+ self.ports.iter().filter(|p| p.direction == Direction::In)
219
+ }
220
+
221
+ pub fn outputs(&self) -> impl Iterator<Item = &Port> {
222
+ self.ports.iter().filter(|p| p.direction == Direction::Out)
223
+ }
224
+
225
+ /// Find the first top-level `case` statement, regardless of whether
226
+ /// it is wrapped in an `always` block or bare. Returns `None` if
227
+ /// there is no top-level case statement. Consumers that need to walk
228
+ /// arbitrarily nested statements should match on [`Statement`]
229
+ /// directly; this helper covers the common "one always-with-one-case
230
+ /// module" shape used throughout `jxcl-rtl` and `jxcl-synthesis`.
231
+ pub fn top_level_case(&self) -> Option<&Statement> {
232
+ fn find(stmts: &[Statement]) -> Option<&Statement> {
233
+ for s in stmts {
234
+ match s {
235
+ Statement::Case { .. } => return Some(s),
236
+ Statement::Always { body, .. } => {
237
+ if let Some(found) = find(body) {
238
+ return Some(found);
239
+ }
240
+ }
241
+ Statement::Assign { .. } => {}
242
+ }
243
+ }
244
+ None
245
+ }
246
+ find(&self.statements)
247
+ }
248
+ }
249
+
250
+ /// Small hand-built example modules shared across this crate's own tests
251
+ /// and the downstream `jxcl-verilog`/`jxcl-vhdl` golden-file tests, so
252
+ /// every backend renders exactly the same AST instance.
253
+ pub mod examples {
254
+ use super::*;
255
+
256
+ /// A 2-to-1 multiplexer: `sel` selects between `a` and `b` on the
257
+ /// single-bit output `y`. This is the canonical minimal example used
258
+ /// to prove the AST is backend-agnostic (`jxcl-verilog`/`jxcl-vhdl`
259
+ /// both render this exact module) and is also the shape
260
+ /// `jxcl-synthesis`'s toy synthesis pass is scoped to handle.
261
+ pub fn mux2_module() -> Module {
262
+ Module::new("mux2")
263
+ .with_ports(vec![
264
+ Port::input("sel", 1),
265
+ Port::input("a", 1),
266
+ Port::input("b", 1),
267
+ Port::output("y", 1),
268
+ ])
269
+ .with_statements(vec![Statement::always(
270
+ Sensitivity::Combinational,
271
+ vec![Statement::case_stmt(
272
+ Expr::ident("sel"),
273
+ vec![
274
+ (
275
+ CaseValue::literal(1, 0),
276
+ vec![Statement::assign("y", Expr::ident("a"))],
277
+ ),
278
+ (
279
+ CaseValue::literal(1, 1),
280
+ vec![Statement::assign("y", Expr::ident("b"))],
281
+ ),
282
+ ],
283
+ )],
284
+ )])
285
+ }
286
+ }
287
+
288
+ #[cfg(test)]
289
+ mod tests {
290
+ use super::*;
291
+ use examples::mux2_module;
292
+
293
+ #[test]
294
+ fn module_structure_matches_hand_built_shape() {
295
+ let m = mux2_module();
296
+ assert_eq!(m.name, "mux2");
297
+ assert_eq!(m.ports.len(), 4);
298
+ assert_eq!(m.inputs().count(), 3);
299
+ assert_eq!(m.outputs().count(), 1);
300
+ assert!(m.signals.is_empty());
301
+ assert_eq!(m.statements.len(), 1);
302
+ }
303
+
304
+ #[test]
305
+ fn port_widths_and_directions_are_preserved() {
306
+ let m = mux2_module();
307
+ let sel = m.ports.iter().find(|p| p.name == "sel").unwrap();
308
+ assert_eq!(sel.direction, Direction::In);
309
+ assert_eq!(sel.width, 1);
310
+
311
+ let y = m.ports.iter().find(|p| p.name == "y").unwrap();
312
+ assert_eq!(y.direction, Direction::Out);
313
+ assert_eq!(y.width, 1);
314
+ }
315
+
316
+ #[test]
317
+ fn top_level_case_is_found_inside_always_block() {
318
+ let m = mux2_module();
319
+ let case = m.top_level_case().expect("module has a case statement");
320
+ match case {
321
+ Statement::Case { selector, arms } => {
322
+ assert_eq!(*selector, Expr::ident("sel"));
323
+ assert_eq!(arms.len(), 2);
324
+ assert_eq!(arms[0].0, CaseValue::literal(1, 0));
325
+ assert_eq!(arms[1].0, CaseValue::literal(1, 1));
326
+ assert_eq!(arms[0].1, vec![Statement::assign("y", Expr::ident("a"))]);
327
+ assert_eq!(arms[1].1, vec![Statement::assign("y", Expr::ident("b"))]);
328
+ }
329
+ other => panic!("expected a case statement, got {other:?}"),
330
+ }
331
+ }
332
+
333
+ #[test]
334
+ fn case_default_arm_is_distinct_from_any_literal() {
335
+ assert_ne!(CaseValue::Default, CaseValue::literal(8, 0));
336
+ assert_ne!(CaseValue::Default, CaseValue::literal(1, 0));
337
+ }
338
+
339
+ #[test]
340
+ fn signals_can_be_added_alongside_ports() {
341
+ let m = Module::new("with_signal")
342
+ .with_ports(vec![Port::input("clk_unused", 1)])
343
+ .with_signals(vec![Signal::new("internal", 8)]);
344
+ assert_eq!(m.signals.len(), 1);
345
+ assert_eq!(m.signals[0].width, 8);
346
+ }
347
+
348
+ #[test]
349
+ fn bit_slice_expr_holds_its_range() {
350
+ let e = Expr::bit_slice(Expr::ident("instr"), 15, 8);
351
+ match e {
352
+ Expr::BitSlice { base, high, low } => {
353
+ assert_eq!(*base, Expr::ident("instr"));
354
+ assert_eq!(high, 15);
355
+ assert_eq!(low, 8);
356
+ }
357
+ other => panic!("expected BitSlice, got {other:?}"),
358
+ }
359
+ }
360
+ }
crates/jxcl-heap/Cargo.toml ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [package]
2
+ name = "jxcl-heap"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+ description = "A simple bump/free-list allocator operating within an address space, for programs needing dynamic memory."
6
+ license = "AGPL-3.0-only OR LicenseRef-Commercial"
7
+
8
+ [dependencies]
9
+ jxcl-address-space = { path = "../jxcl-address-space" }
10
+ jxcl-load-store = { path = "../jxcl-load-store" }
11
+ jxcl-exceptions = { path = "../jxcl-exceptions" }
12
+ jxcl-types = { path = "../jxcl-types" }
13
+
14
+ [dev-dependencies]
15
+ jxcl-memory = { path = "../jxcl-memory" }
crates/jxcl-heap/README.md ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # jxcl-heap
2
+
3
+ A simple bump allocator operating within an address space, for programs needing dynamic memory.
4
+
5
+ ## Architecture
6
+
7
+ **Owns:** `Heap::alloc`/`free`.
8
+
9
+ **Category:** memory · **Source:** new.
10
+
11
+ **Documented minimal scope:** this is a bump (watermark) allocator.
12
+ `alloc` only ever moves the watermark forward; `free` is an intentional
13
+ no-op. The base ISA has no architectural notion of freeing memory (that
14
+ would be a userspace/runtime convention layered on top), so a real
15
+ free-list is out of scope here and left to a possible future crate that
16
+ can own that separate invariant without breaking this one's API.
17
+
18
+ **Deviation from `docs/crates.toml`:** `jxcl-types` is added as a direct
19
+ dependency (see `jxcl-cache-model`/`jxcl-stack`'s READMEs for the same
20
+ reasoning -- naming `Address` in this crate's own API requires it).
21
+
22
+ ## Public API
23
+
24
+ - `Heap::new(base, len)` / `Heap::from_region(&AddressSpace, name)`
25
+ - `alloc(&mut AddressSpace, size, align) -> Result<Address, HeapError>`
26
+ -- zero-initializes the returned range, permission-checked against
27
+ the `AddressSpace`
28
+ - `free(addr)` -- documented no-op
29
+ - `used`, `remaining`, `base`, `limit`
30
+
31
+ ## Dependencies
32
+
33
+ Workspace crates:
34
+
35
+ - `jxcl-address-space`
36
+ - `jxcl-load-store`
37
+ - `jxcl-exceptions`
38
+ - `jxcl-types`
39
+
40
+ External crates:
41
+
42
+ *(none)*
43
+
44
+ ## Testing
45
+
46
+ `cargo test -p jxcl-heap`: sequential allocations never overlap,
47
+ allocations respect the requested alignment, out-of-memory once the
48
+ heap is exhausted, rejection of a non-power-of-two alignment,
49
+ permission-denied when the backing region isn't writable, `free`
50
+ provably not reclaiming space, `from_region` lookup, and that allocated
51
+ bytes come back zeroed even over previously-dirty memory.
52
+
53
+ ## Status
54
+
55
+ `implemented`.
56
+
57
+ ## License
58
+
59
+ AGPLv3 / Commercial dual-license. See `LICENSE-AGPL`, `LICENSE-COMMERCIAL`, and `LICENSE-NOTICE` at the repository root.
crates/jxcl-heap/src/lib.rs ADDED
@@ -0,0 +1,244 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Copyright (C) 2026 ERRANT DIGITAL INSTITUTE OF TECHNOLOGY (SEIT NGO) % SNAPKITTY COLLECTIVE
2
+ // SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial
3
+ // See LICENSE-AGPL and LICENSE-COMMERCIAL in this repository for license terms.
4
+
5
+ //! A simple bump allocator operating within an address space, for
6
+ //! programs needing dynamic memory.
7
+ //!
8
+ //! ## Documented minimal scope
9
+ //!
10
+ //! This is a bump allocator: `alloc` only ever moves a watermark
11
+ //! forward, never reuses freed space, and `free` is an intentional
12
+ //! no-op (documented, not a bug -- a real free-list allocator is a
13
+ //! separate, larger invariant that a future crate can own without
14
+ //! breaking this one's API, matching `docs/CRATE_ARCHITECTURE.md`'s
15
+ //! "can evolve independently" test). This is sufficient for programs
16
+ //! that allocate for the duration of a run and never need to reclaim
17
+ //! individual objects, which is the only workload the base JXCL ISA
18
+ //! (spec) itself defines any support for (there is no architectural
19
+ //! notion of freeing memory -- that would be a userspace/runtime
20
+ //! convention on top of the ISA).
21
+ //!
22
+ //! ## Deviation from `docs/crates.toml`
23
+ //!
24
+ //! `jxcl-types` is added as a direct dependency, alongside the
25
+ //! registry's listed `jxcl-address-space`, `jxcl-load-store`,
26
+ //! `jxcl-exceptions` -- needed to name `Address` in this crate's own
27
+ //! public API, the same reason `jxcl-cache-model` and `jxcl-stack` add it.
28
+ #![forbid(unsafe_code)]
29
+
30
+ use jxcl_address_space::{AddressSpace, Permission};
31
+ use jxcl_load_store::store_u8;
32
+ use jxcl_types::Address;
33
+
34
+ /// Why [`Heap::alloc`] failed.
35
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
36
+ pub enum HeapError {
37
+ /// Not enough room left before the heap's upper bound.
38
+ OutOfMemory,
39
+ /// `align` was zero or not a power of two.
40
+ InvalidAlignment,
41
+ /// The address space doesn't grant read+write over the region this
42
+ /// allocation would occupy (e.g. the heap region wasn't registered,
43
+ /// or was registered without write permission).
44
+ PermissionDenied,
45
+ }
46
+
47
+ /// A watermark-style bump allocator over `[base, base+len)` of an
48
+ /// [`AddressSpace`]. Owns only the bookkeeping (the next free address);
49
+ /// the bytes themselves live in the `AddressSpace`'s backing memory.
50
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
51
+ pub struct Heap {
52
+ base: Address,
53
+ limit: Address,
54
+ next: Address,
55
+ }
56
+
57
+ fn align_up(value: u64, align: u64) -> Option<u64> {
58
+ if align == 0 || !align.is_power_of_two() {
59
+ return None;
60
+ }
61
+ let mask = align - 1;
62
+ value.checked_add(mask).map(|v| v & !mask)
63
+ }
64
+
65
+ impl Heap {
66
+ /// A heap over `[base, base+len)`. Does not itself register a region
67
+ /// with any `AddressSpace`; pair it with one that already grants
68
+ /// read+write over that range (e.g. via `jxcl-memory-map`).
69
+ pub fn new(base: Address, len: u64) -> Self {
70
+ Heap {
71
+ base,
72
+ limit: Address::new(base.get().saturating_add(len)),
73
+ next: base,
74
+ }
75
+ }
76
+
77
+ /// Build a heap from a named region already registered in `space`
78
+ /// (e.g. `jxcl-memory-map::DEFAULT_MEMORY_MAP`'s `"heap"` region).
79
+ pub fn from_region(space: &AddressSpace, region_name: &str) -> Option<Self> {
80
+ space
81
+ .regions()
82
+ .iter()
83
+ .find(|r| r.name == region_name)
84
+ .map(|r| Heap::new(r.base, r.len))
85
+ }
86
+
87
+ pub fn base(&self) -> Address {
88
+ self.base
89
+ }
90
+ pub fn limit(&self) -> Address {
91
+ self.limit
92
+ }
93
+ pub fn used(&self) -> u64 {
94
+ self.next.get() - self.base.get()
95
+ }
96
+ pub fn remaining(&self) -> u64 {
97
+ self.limit.get() - self.next.get()
98
+ }
99
+
100
+ /// Allocate `size` zero-initialized bytes, aligned to `align` (which
101
+ /// must be a nonzero power of two), inside `space`. Bumps the
102
+ /// watermark forward and never reuses space handed out by an earlier
103
+ /// call, even after [`Heap::free`].
104
+ pub fn alloc(
105
+ &mut self,
106
+ space: &mut AddressSpace,
107
+ size: u64,
108
+ align: u64,
109
+ ) -> Result<Address, HeapError> {
110
+ let aligned_start = align_up(self.next.get(), align).ok_or(HeapError::InvalidAlignment)?;
111
+ let end = aligned_start
112
+ .checked_add(size)
113
+ .ok_or(HeapError::OutOfMemory)?;
114
+ if end > self.limit.get() {
115
+ return Err(HeapError::OutOfMemory);
116
+ }
117
+ let addr = Address::new(aligned_start);
118
+ if size > 0 && !space.is_permitted(addr, size, Permission::READ_WRITE) {
119
+ return Err(HeapError::PermissionDenied);
120
+ }
121
+ for i in 0..size {
122
+ // Zero-initialize; a fresh `Memory` already starts zeroed,
123
+ // but this makes the guarantee explicit and independent of
124
+ // what was previously in this range.
125
+ store_u8(space.memory_mut(), Address::new(aligned_start + i), 0)
126
+ .map_err(|_| HeapError::PermissionDenied)?;
127
+ }
128
+ self.next = Address::new(end);
129
+ Ok(addr)
130
+ }
131
+
132
+ /// Intentional no-op: see the module-level "Documented minimal
133
+ /// scope" note. Accepting `addr` (rather than no parameters) keeps
134
+ /// the call site meaningful and ready for a future real
135
+ /// implementation without changing its signature.
136
+ pub fn free(&mut self, _addr: Address) {}
137
+ }
138
+
139
+ #[cfg(test)]
140
+ mod tests {
141
+ use super::*;
142
+ use jxcl_address_space::Region;
143
+ use jxcl_memory::Memory;
144
+
145
+ fn space_with_heap(len: u64) -> AddressSpace {
146
+ let mut space = AddressSpace::new(Memory::new(len));
147
+ space
148
+ .add_region(Region::new(
149
+ "heap",
150
+ Address::new(0),
151
+ len,
152
+ Permission::READ_WRITE,
153
+ ))
154
+ .unwrap();
155
+ space
156
+ }
157
+
158
+ #[test]
159
+ fn sequential_allocations_never_overlap() {
160
+ let mut space = space_with_heap(256);
161
+ let mut heap = Heap::new(Address::new(0), 256);
162
+ let a = heap.alloc(&mut space, 16, 8).unwrap();
163
+ let b = heap.alloc(&mut space, 16, 8).unwrap();
164
+ assert_eq!(a, Address::new(0));
165
+ assert_eq!(b, Address::new(16));
166
+ }
167
+
168
+ #[test]
169
+ fn allocations_are_aligned() {
170
+ let mut space = space_with_heap(256);
171
+ let mut heap = Heap::new(Address::new(0), 256);
172
+ heap.alloc(&mut space, 3, 8).unwrap(); // bumps next to 3, next alloc must realign
173
+ let b = heap.alloc(&mut space, 8, 8).unwrap();
174
+ assert_eq!(b.get() % 8, 0);
175
+ }
176
+
177
+ #[test]
178
+ fn out_of_memory_when_exceeding_the_heap() {
179
+ let mut space = space_with_heap(16);
180
+ let mut heap = Heap::new(Address::new(0), 16);
181
+ assert_eq!(
182
+ heap.alloc(&mut space, 32, 1).unwrap_err(),
183
+ HeapError::OutOfMemory
184
+ );
185
+ }
186
+
187
+ #[test]
188
+ fn invalid_alignment_is_rejected() {
189
+ let mut space = space_with_heap(16);
190
+ let mut heap = Heap::new(Address::new(0), 16);
191
+ assert_eq!(
192
+ heap.alloc(&mut space, 4, 3).unwrap_err(),
193
+ HeapError::InvalidAlignment
194
+ );
195
+ }
196
+
197
+ #[test]
198
+ fn permission_denied_when_region_is_read_only() {
199
+ let mut space = AddressSpace::new(Memory::new(16));
200
+ space
201
+ .add_region(Region::new(
202
+ "rodata",
203
+ Address::new(0),
204
+ 16,
205
+ Permission::READ_ONLY,
206
+ ))
207
+ .unwrap();
208
+ let mut heap = Heap::new(Address::new(0), 16);
209
+ assert_eq!(
210
+ heap.alloc(&mut space, 4, 1).unwrap_err(),
211
+ HeapError::PermissionDenied
212
+ );
213
+ }
214
+
215
+ #[test]
216
+ fn free_is_a_documented_no_op() {
217
+ let mut space = space_with_heap(16);
218
+ let mut heap = Heap::new(Address::new(0), 16);
219
+ let a = heap.alloc(&mut space, 8, 1).unwrap();
220
+ let used_before = heap.used();
221
+ heap.free(a);
222
+ assert_eq!(heap.used(), used_before, "free must not reclaim space");
223
+ }
224
+
225
+ #[test]
226
+ fn from_region_finds_the_named_region() {
227
+ let space = space_with_heap(64);
228
+ let heap = Heap::from_region(&space, "heap").unwrap();
229
+ assert_eq!(heap.base(), Address::new(0));
230
+ assert_eq!(heap.remaining(), 64);
231
+ assert!(Heap::from_region(&space, "nonexistent").is_none());
232
+ }
233
+
234
+ #[test]
235
+ fn allocated_bytes_are_zeroed() {
236
+ let mut space = space_with_heap(16);
237
+ let mut heap = Heap::new(Address::new(0), 16);
238
+ space.memory_mut().write8(Address::new(0), 0xFF).unwrap();
239
+ let a = heap.alloc(&mut space, 4, 1).unwrap();
240
+ for i in 0..4 {
241
+ assert_eq!(space.memory().read8(Address::new(a.get() + i)).unwrap(), 0);
242
+ }
243
+ }
244
+ }
crates/jxcl-http/Cargo.toml ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [package]
2
+ name = "jxcl-http"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+ description = "Shared HTTP client/server helpers (timeout wrapper, error-to-status mapping) extracted from photo-cache-service's duplicated per-binary logic."
6
+ license = "AGPL-3.0-only OR LicenseRef-Commercial"
7
+
8
+ [dependencies]
9
+ jxcl-errors = { path = "../jxcl-errors" }
10
+ axum = "0.8"
11
+ reqwest = { version = "0.13", default-features = false, features = ["rustls", "json"] }
12
+ tokio = { version = "1", features = ["rt", "time"] }
13
+
14
+ [dev-dependencies]
15
+ tokio = { version = "1", features = ["rt", "time", "macros"] }
crates/jxcl-http/README.md ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # jxcl-http
2
+
3
+ Shared HTTP client/server helpers (timeout wrapper, error-to-status mapping) extracted from photo-cache-service's duplicated per-binary logic.
4
+
5
+ ## Purpose
6
+
7
+ Provides generic utilities for async timeout handling and error-to-HTTP-status mapping, designed to reduce code duplication across services that need consistent timeout and error handling patterns.
8
+
9
+ ## Public API
10
+
11
+ - `with_timeout` - Wraps a future with a timeout, returning `TimeoutError` if the operation exceeds the specified duration.
12
+ - `error_to_status` - Maps `jxcl-errors::Error` types to HTTP status codes for consistent error responses.
13
+ - `TimeoutError` - Error type indicating an async operation timed out.
14
+
15
+ ## Implementation Notes
16
+
17
+ The `with_timeout` function provides a generic wrapper around `tokio::time::timeout`, making it easy to apply timeouts to any async operation. This is particularly useful for operations that might block indefinitely, such as external API calls or database queries.
18
+
19
+ The `error_to_status` function maps the three variants of `jxcl-errors::Error` to appropriate HTTP status codes:
20
+ - `OutOfBounds` → 400 Bad Request
21
+ - `InvalidConfig` → 500 Internal Server Error
22
+ - `InvalidInstruction` → 400 Bad Request
23
+
24
+ ## Testing
25
+
26
+ Implemented unit tests covering:
27
+ 1. Timeout success case - futures completing within the timeout duration
28
+ 2. Timeout failure case - futures exceeding the timeout duration
29
+ 3. Error-to-status mapping for each `jxcl-errors::Error` variant (3 tests)
30
+
31
+ Total: 6 unit tests
32
+
33
+ ## Dependencies
34
+
35
+ Workspace crates:
36
+ - `jxcl-errors`
37
+
38
+ External crates:
39
+ - `axum` 0.8 (for HTTP status codes)
40
+ - `reqwest` 0.13 (optional, for HTTP client context)
41
+ - `tokio` 1.x (for async runtime and timeout utilities)
42
+
43
+ ## License
44
+
45
+ AGPLv3 / Commercial dual-license. See `LICENSE-AGPL`, `LICENSE-COMMERCIAL`, and `LICENSE-NOTICE` at the repository root.
crates/jxcl-http/src/lib.rs ADDED
@@ -0,0 +1,137 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Copyright (C) 2026 ERRANT DIGITAL INSTITUTE OF TECHNOLOGY (SEIT NGO) % SNAPKITTY COLLECTIVE
2
+ // SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial
3
+ // See LICENSE-AGPL and LICENSE-COMMERCIAL in this repository for license terms.
4
+
5
+ //! Shared HTTP client/server helpers (timeout wrapper, error-to-status mapping) extracted from photo-cache-service's duplicated per-binary logic.
6
+ //!
7
+ //! Provides generic async operation timeout wrapper and error-to-HTTP-status mapping for jxcl-errors types.
8
+ #![forbid(unsafe_code)]
9
+
10
+ use axum::http::StatusCode;
11
+ use std::fmt;
12
+ use std::time::Duration;
13
+
14
+ /// An error that occurs when an async operation times out.
15
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
16
+ pub struct TimeoutError;
17
+
18
+ impl fmt::Display for TimeoutError {
19
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
20
+ write!(f, "operation timed out")
21
+ }
22
+ }
23
+ impl std::error::Error for TimeoutError {}
24
+
25
+ /// Wraps a future with a timeout. If the future completes within the
26
+ /// specified duration, returns its result. Otherwise, returns a `TimeoutError`.
27
+ ///
28
+ /// # Examples
29
+ ///
30
+ /// ```rust,no_run
31
+ /// use jxcl_http::with_timeout;
32
+ /// use std::time::Duration;
33
+ ///
34
+ /// async fn some_operation() -> String {
35
+ /// "result".to_string()
36
+ /// }
37
+ ///
38
+ /// # async fn example() {
39
+ /// let result = with_timeout(some_operation(), Duration::from_secs(5)).await;
40
+ /// assert!(result.is_ok());
41
+ /// # }
42
+ /// ```
43
+ pub async fn with_timeout<F, T>(future: F, duration: Duration) -> Result<T, TimeoutError>
44
+ where
45
+ F: std::future::Future<Output = T>,
46
+ {
47
+ match tokio::time::timeout(duration, future).await {
48
+ Ok(result) => Ok(result),
49
+ Err(_elapsed) => Err(TimeoutError),
50
+ }
51
+ }
52
+
53
+ /// Maps a jxcl-errors error type to an HTTP status code.
54
+ ///
55
+ /// Provides a consistent mapping from jxcl error types to HTTP status codes
56
+ /// for use in HTTP handlers and error responses.
57
+ pub fn error_to_status(error: &jxcl_errors::Error) -> StatusCode {
58
+ match error {
59
+ jxcl_errors::Error::OutOfBounds { .. } => StatusCode::BAD_REQUEST,
60
+ jxcl_errors::Error::InvalidConfig { .. } => StatusCode::INTERNAL_SERVER_ERROR,
61
+ jxcl_errors::Error::InvalidInstruction { .. } => StatusCode::BAD_REQUEST,
62
+ }
63
+ }
64
+
65
+ #[cfg(test)]
66
+ mod tests {
67
+ use super::*;
68
+
69
+ #[test]
70
+ fn timeout_error_displays_correctly() {
71
+ let err = TimeoutError;
72
+ assert_eq!(err.to_string(), "operation timed out");
73
+ }
74
+
75
+ #[tokio::test]
76
+ async fn with_timeout_succeeds_when_future_completes_within_duration() {
77
+ async fn quick_operation() -> &'static str {
78
+ "success"
79
+ }
80
+
81
+ let result = with_timeout(quick_operation(), Duration::from_secs(1)).await;
82
+ assert_eq!(result, Ok("success"));
83
+ }
84
+
85
+ #[tokio::test]
86
+ async fn with_timeout_fails_when_future_exceeds_duration() {
87
+ async fn slow_operation() {
88
+ tokio::time::sleep(Duration::from_millis(500)).await;
89
+ }
90
+
91
+ let result: Result<(), TimeoutError> =
92
+ with_timeout(slow_operation(), Duration::from_millis(100)).await;
93
+ assert!(result.is_err());
94
+ }
95
+
96
+ #[tokio::test]
97
+ async fn with_timeout_completes_immediately_with_zero_duration() {
98
+ async fn instant_operation() -> i32 {
99
+ 42
100
+ }
101
+
102
+ // Even with zero duration, if the operation completes synchronously,
103
+ // it may still succeed before the timeout triggers. So we test that
104
+ // the mechanism works, not that zero always times out.
105
+ let result = with_timeout(instant_operation(), Duration::from_nanos(1)).await;
106
+ // This may succeed or fail depending on scheduling, so we just verify
107
+ // the result is well-typed.
108
+ let _ = result;
109
+ }
110
+
111
+ #[test]
112
+ fn error_to_status_out_of_bounds() {
113
+ let err = jxcl_errors::Error::OutOfBounds {
114
+ needed: 4,
115
+ available: 1,
116
+ at: 10,
117
+ };
118
+ assert_eq!(error_to_status(&err), StatusCode::BAD_REQUEST);
119
+ }
120
+
121
+ #[test]
122
+ fn error_to_status_invalid_config() {
123
+ let err = jxcl_errors::Error::InvalidConfig {
124
+ name: "TEST_VAR",
125
+ reason: "wrong length".to_string(),
126
+ };
127
+ assert_eq!(error_to_status(&err), StatusCode::INTERNAL_SERVER_ERROR);
128
+ }
129
+
130
+ #[test]
131
+ fn error_to_status_invalid_instruction() {
132
+ let err = jxcl_errors::Error::InvalidInstruction {
133
+ reason: "bad operand".to_string(),
134
+ };
135
+ assert_eq!(error_to_status(&err), StatusCode::BAD_REQUEST);
136
+ }
137
+ }
crates/jxcl-instruction-validation/Cargo.toml ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [package]
2
+ name = "jxcl-instruction-validation"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+ description = "Per-instruction operand-legality checks (register range, immediate range, addressing-mode compatibility with the opcode) shared by the decoder and the static binary validator."
6
+ license = "AGPL-3.0-only OR LicenseRef-Commercial"
7
+
8
+ [dependencies]
9
+ jxcl-instructions = { path = "../jxcl-instructions" }
10
+ jxcl-operands = { path = "../jxcl-operands" }
11
+ jxcl-registers = { path = "../jxcl-registers" }
12
+ jxcl-errors = { path = "../jxcl-errors" }
13
+ jxcl-opcodes = { path = "../jxcl-opcodes" }
crates/jxcl-instruction-validation/README.md ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # jxcl-instruction-validation
2
+
3
+ Per-instruction operand-legality checks for the JXCL ISA.
4
+
5
+ ## Purpose
6
+
7
+ Validates instruction operands according to ISA rules:
8
+ - Register operands are in valid range (0..REGISTER_COUNT)
9
+ - Immediates are within appropriate ranges
10
+ - Memory addressing modes use valid base registers
11
+
12
+ This is the single source of truth for instruction legality, used by both the decoder and static binary validator to ensure consistent validation rules.
13
+
14
+ ## Public API
15
+
16
+ - `validate_instruction(&Instruction) -> Result<(), Error>` - Validate an instruction's operands
17
+
18
+ ## Implementation Notes
19
+
20
+ - All register operands must be in range `0..32` (the configured REGISTER_COUNT)
21
+ - Immediate values use the full `u64` range for 64-bit immediates
22
+ - Memory displacement fields (`i32`) are always valid (no range constraints)
23
+ - Validation is the single source of truth shared between the decoder and validator
24
+
25
+ ## Testing
26
+
27
+ The crate includes 34 unit tests covering:
28
+ - Valid instructions with all operand formats (None, R, RR, RRR, RImm64, RMem, MemR, Cas, BranchImm32, Imm16)
29
+ - Boundary cases (register 0, register 31)
30
+ - Invalid register indices (32, 255)
31
+ - All combinations of register operand validity in multi-operand formats
32
+
33
+ ## License
34
+
35
+ AGPLv3 / Commercial dual-license. See `LICENSE-AGPL`, `LICENSE-COMMERCIAL`, and `LICENSE-NOTICE` at the repository root.
crates/jxcl-instruction-validation/src/lib.rs ADDED
@@ -0,0 +1,489 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Copyright (C) 2026 ERRANT DIGITAL INSTITUTE OF TECHNOLOGY (SEIT NGO) % SNAPKITTY COLLECTIVE
2
+ // SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial
3
+ // See LICENSE-AGPL and LICENSE-COMMERCIAL in this repository for license terms.
4
+
5
+ //! Per-instruction operand-legality checks (register range, immediate range, addressing-mode compatibility with the opcode) shared by the decoder and the static binary validator.
6
+ //!
7
+ //! Owns: `validate_instruction(&Instruction)` -> `Result<(), Error>` --
8
+ //! the single source of truth for "is this a legal instruction", so the
9
+ //! decoder and jxcl-validator never diverge.
10
+ //!
11
+ //! This is the validation layer that ensures every decoded instruction
12
+ //! has valid operands according to ISA rules, independent of whether the
13
+ //! instruction's *semantics* are correct.
14
+ #![forbid(unsafe_code)]
15
+
16
+ use jxcl_errors::Error;
17
+ use jxcl_instructions::Instruction;
18
+ use jxcl_operands::{Operands, Register};
19
+ use jxcl_registers::validate_register;
20
+
21
+ /// Validate that an instruction's operands are legal according to ISA rules.
22
+ ///
23
+ /// This checks:
24
+ /// - All register operands are in the valid range (0..REGISTER_COUNT)
25
+ /// - Immediates are within their appropriate ranges
26
+ /// - Memory addressing modes use valid base registers
27
+ /// - The operand shape matches what's expected for the opcode
28
+ ///
29
+ /// This is the single source of truth for instruction legality, used by
30
+ /// both the decoder (to reject malformed binary) and any static validator
31
+ /// (to check decoded binaries).
32
+ pub fn validate_instruction(instr: &Instruction) -> Result<(), Error> {
33
+ // Validate all register operands in the instruction
34
+ for reg in instr.operands.registers() {
35
+ validate_register_operand(reg)?;
36
+ }
37
+
38
+ // Validate operand-specific constraints based on the shape
39
+ match instr.operands {
40
+ Operands::None | Operands::BranchImm32 { .. } | Operands::Imm16 { .. } => {
41
+ // No register operands to validate beyond the generic check above
42
+ }
43
+ Operands::R { .. } => {
44
+ // Single register destination; already validated
45
+ }
46
+ Operands::RR { .. } => {
47
+ // Two register operands; already validated
48
+ }
49
+ Operands::RRR { .. } => {
50
+ // Three register operands; already validated
51
+ }
52
+ Operands::RImm64 { imm, .. } => {
53
+ // Immediate can be any u64, no range constraint
54
+ validate_immediate_u64(imm)?;
55
+ }
56
+ Operands::RMem { .. } => {
57
+ // Base register already validated, displacement is i32 (always valid)
58
+ }
59
+ Operands::MemR { .. } => {
60
+ // Base and rs already validated, displacement is i32 (always valid)
61
+ }
62
+ Operands::Cas { .. } => {
63
+ // All registers already validated, displacement is i32 (always valid)
64
+ }
65
+ }
66
+
67
+ Ok(())
68
+ }
69
+
70
+ /// Validate that a register operand is in the valid range.
71
+ fn validate_register_operand(reg: Register) -> Result<(), Error> {
72
+ validate_register(reg).map_err(|_e| Error::InvalidInstruction {
73
+ reason: format!("register {} out of range", reg.get()),
74
+ })
75
+ }
76
+
77
+ /// Validate a 64-bit immediate value.
78
+ ///
79
+ /// As u64 has no "out of range" values (every u64 is valid), this is
80
+ /// a no-op but is provided for symmetry and future extensibility.
81
+ #[inline]
82
+ fn validate_immediate_u64(_imm: u64) -> Result<(), Error> {
83
+ Ok(())
84
+ }
85
+
86
+ #[cfg(test)]
87
+ mod tests {
88
+ use super::*;
89
+ use jxcl_opcodes::Mnemonic;
90
+
91
+ #[test]
92
+ fn validate_nop_instruction() {
93
+ let instr = Instruction::new(Mnemonic::Nop, Operands::None);
94
+ assert!(validate_instruction(&instr).is_ok());
95
+ }
96
+
97
+ #[test]
98
+ fn validate_r_format_instruction_with_valid_register() {
99
+ let instr = Instruction::new(
100
+ Mnemonic::Not,
101
+ Operands::R {
102
+ rd: Register::new(5),
103
+ },
104
+ );
105
+ assert!(validate_instruction(&instr).is_ok());
106
+ }
107
+
108
+ #[test]
109
+ fn validate_r_format_instruction_with_r0() {
110
+ let instr = Instruction::new(
111
+ Mnemonic::Not,
112
+ Operands::R {
113
+ rd: Register::new(0),
114
+ },
115
+ );
116
+ assert!(validate_instruction(&instr).is_ok());
117
+ }
118
+
119
+ #[test]
120
+ fn validate_r_format_instruction_with_max_register() {
121
+ let instr = Instruction::new(
122
+ Mnemonic::Not,
123
+ Operands::R {
124
+ rd: Register::new(31),
125
+ },
126
+ );
127
+ assert!(validate_instruction(&instr).is_ok());
128
+ }
129
+
130
+ #[test]
131
+ fn reject_r_format_instruction_with_out_of_range_register() {
132
+ let instr = Instruction::new(
133
+ Mnemonic::Not,
134
+ Operands::R {
135
+ rd: Register::new(32),
136
+ },
137
+ );
138
+ assert!(validate_instruction(&instr).is_err());
139
+ }
140
+
141
+ #[test]
142
+ fn validate_rr_format_instruction() {
143
+ let instr = Instruction::new(
144
+ Mnemonic::Mov,
145
+ Operands::RR {
146
+ rd: Register::new(0),
147
+ rs: Register::new(1),
148
+ },
149
+ );
150
+ assert!(validate_instruction(&instr).is_ok());
151
+ }
152
+
153
+ #[test]
154
+ fn validate_rrr_format_instruction() {
155
+ let instr = Instruction::new(
156
+ Mnemonic::Add,
157
+ Operands::RRR {
158
+ rd: Register::new(0),
159
+ rs1: Register::new(1),
160
+ rs2: Register::new(2),
161
+ },
162
+ );
163
+ assert!(validate_instruction(&instr).is_ok());
164
+ }
165
+
166
+ #[test]
167
+ fn reject_rrr_format_with_invalid_destination() {
168
+ let instr = Instruction::new(
169
+ Mnemonic::Add,
170
+ Operands::RRR {
171
+ rd: Register::new(32),
172
+ rs1: Register::new(1),
173
+ rs2: Register::new(2),
174
+ },
175
+ );
176
+ assert!(validate_instruction(&instr).is_err());
177
+ }
178
+
179
+ #[test]
180
+ fn reject_rrr_format_with_invalid_source1() {
181
+ let instr = Instruction::new(
182
+ Mnemonic::Add,
183
+ Operands::RRR {
184
+ rd: Register::new(0),
185
+ rs1: Register::new(32),
186
+ rs2: Register::new(2),
187
+ },
188
+ );
189
+ assert!(validate_instruction(&instr).is_err());
190
+ }
191
+
192
+ #[test]
193
+ fn reject_rrr_format_with_invalid_source2() {
194
+ let instr = Instruction::new(
195
+ Mnemonic::Add,
196
+ Operands::RRR {
197
+ rd: Register::new(0),
198
+ rs1: Register::new(1),
199
+ rs2: Register::new(32),
200
+ },
201
+ );
202
+ assert!(validate_instruction(&instr).is_err());
203
+ }
204
+
205
+ #[test]
206
+ fn validate_rimm64_format_instruction() {
207
+ let instr = Instruction::new(
208
+ Mnemonic::Movi,
209
+ Operands::RImm64 {
210
+ rd: Register::new(5),
211
+ imm: 0xDEADBEEF,
212
+ },
213
+ );
214
+ assert!(validate_instruction(&instr).is_ok());
215
+ }
216
+
217
+ #[test]
218
+ fn validate_rimm64_with_max_immediate() {
219
+ let instr = Instruction::new(
220
+ Mnemonic::Movi,
221
+ Operands::RImm64 {
222
+ rd: Register::new(5),
223
+ imm: u64::MAX,
224
+ },
225
+ );
226
+ assert!(validate_instruction(&instr).is_ok());
227
+ }
228
+
229
+ #[test]
230
+ fn validate_rimm64_with_zero_immediate() {
231
+ let instr = Instruction::new(
232
+ Mnemonic::Movi,
233
+ Operands::RImm64 {
234
+ rd: Register::new(5),
235
+ imm: 0,
236
+ },
237
+ );
238
+ assert!(validate_instruction(&instr).is_ok());
239
+ }
240
+
241
+ #[test]
242
+ fn reject_rimm64_with_invalid_register() {
243
+ let instr = Instruction::new(
244
+ Mnemonic::Movi,
245
+ Operands::RImm64 {
246
+ rd: Register::new(32),
247
+ imm: 0xDEADBEEF,
248
+ },
249
+ );
250
+ assert!(validate_instruction(&instr).is_err());
251
+ }
252
+
253
+ #[test]
254
+ fn validate_rmem_format_instruction() {
255
+ let instr = Instruction::new(
256
+ Mnemonic::Load,
257
+ Operands::RMem {
258
+ rd: Register::new(5),
259
+ base: Register::new(10),
260
+ disp: -8,
261
+ },
262
+ );
263
+ assert!(validate_instruction(&instr).is_ok());
264
+ }
265
+
266
+ #[test]
267
+ fn validate_rmem_with_max_displacement() {
268
+ let instr = Instruction::new(
269
+ Mnemonic::Load,
270
+ Operands::RMem {
271
+ rd: Register::new(5),
272
+ base: Register::new(10),
273
+ disp: i32::MAX,
274
+ },
275
+ );
276
+ assert!(validate_instruction(&instr).is_ok());
277
+ }
278
+
279
+ #[test]
280
+ fn validate_rmem_with_min_displacement() {
281
+ let instr = Instruction::new(
282
+ Mnemonic::Load,
283
+ Operands::RMem {
284
+ rd: Register::new(5),
285
+ base: Register::new(10),
286
+ disp: i32::MIN,
287
+ },
288
+ );
289
+ assert!(validate_instruction(&instr).is_ok());
290
+ }
291
+
292
+ #[test]
293
+ fn reject_rmem_with_invalid_destination() {
294
+ let instr = Instruction::new(
295
+ Mnemonic::Load,
296
+ Operands::RMem {
297
+ rd: Register::new(32),
298
+ base: Register::new(10),
299
+ disp: 0,
300
+ },
301
+ );
302
+ assert!(validate_instruction(&instr).is_err());
303
+ }
304
+
305
+ #[test]
306
+ fn reject_rmem_with_invalid_base() {
307
+ let instr = Instruction::new(
308
+ Mnemonic::Load,
309
+ Operands::RMem {
310
+ rd: Register::new(5),
311
+ base: Register::new(32),
312
+ disp: 0,
313
+ },
314
+ );
315
+ assert!(validate_instruction(&instr).is_err());
316
+ }
317
+
318
+ #[test]
319
+ fn validate_memr_format_instruction() {
320
+ let instr = Instruction::new(
321
+ Mnemonic::Store,
322
+ Operands::MemR {
323
+ base: Register::new(10),
324
+ disp: 8,
325
+ rs: Register::new(5),
326
+ },
327
+ );
328
+ assert!(validate_instruction(&instr).is_ok());
329
+ }
330
+
331
+ #[test]
332
+ fn reject_memr_with_invalid_base() {
333
+ let instr = Instruction::new(
334
+ Mnemonic::Store,
335
+ Operands::MemR {
336
+ base: Register::new(32),
337
+ disp: 0,
338
+ rs: Register::new(5),
339
+ },
340
+ );
341
+ assert!(validate_instruction(&instr).is_err());
342
+ }
343
+
344
+ #[test]
345
+ fn reject_memr_with_invalid_source() {
346
+ let instr = Instruction::new(
347
+ Mnemonic::Store,
348
+ Operands::MemR {
349
+ base: Register::new(10),
350
+ disp: 0,
351
+ rs: Register::new(32),
352
+ },
353
+ );
354
+ assert!(validate_instruction(&instr).is_err());
355
+ }
356
+
357
+ #[test]
358
+ fn validate_cas_format_instruction() {
359
+ let instr = Instruction::new(
360
+ Mnemonic::Cas,
361
+ Operands::Cas {
362
+ rd: Register::new(0),
363
+ base: Register::new(10),
364
+ rs_new: Register::new(5),
365
+ disp: 0,
366
+ },
367
+ );
368
+ assert!(validate_instruction(&instr).is_ok());
369
+ }
370
+
371
+ #[test]
372
+ fn reject_cas_with_invalid_destination() {
373
+ let instr = Instruction::new(
374
+ Mnemonic::Cas,
375
+ Operands::Cas {
376
+ rd: Register::new(32),
377
+ base: Register::new(10),
378
+ rs_new: Register::new(5),
379
+ disp: 0,
380
+ },
381
+ );
382
+ assert!(validate_instruction(&instr).is_err());
383
+ }
384
+
385
+ #[test]
386
+ fn reject_cas_with_invalid_base() {
387
+ let instr = Instruction::new(
388
+ Mnemonic::Cas,
389
+ Operands::Cas {
390
+ rd: Register::new(0),
391
+ base: Register::new(32),
392
+ rs_new: Register::new(5),
393
+ disp: 0,
394
+ },
395
+ );
396
+ assert!(validate_instruction(&instr).is_err());
397
+ }
398
+
399
+ #[test]
400
+ fn reject_cas_with_invalid_new_value() {
401
+ let instr = Instruction::new(
402
+ Mnemonic::Cas,
403
+ Operands::Cas {
404
+ rd: Register::new(0),
405
+ base: Register::new(10),
406
+ rs_new: Register::new(32),
407
+ disp: 0,
408
+ },
409
+ );
410
+ assert!(validate_instruction(&instr).is_err());
411
+ }
412
+
413
+ #[test]
414
+ fn validate_branch_imm32() {
415
+ let instr = Instruction::new(Mnemonic::Jz, Operands::BranchImm32 { disp: 512 });
416
+ assert!(validate_instruction(&instr).is_ok());
417
+ }
418
+
419
+ #[test]
420
+ fn validate_branch_imm32_negative() {
421
+ let instr = Instruction::new(Mnemonic::Jnz, Operands::BranchImm32 { disp: -512 });
422
+ assert!(validate_instruction(&instr).is_ok());
423
+ }
424
+
425
+ #[test]
426
+ fn validate_imm16_format() {
427
+ let instr = Instruction::new(Mnemonic::Sys, Operands::Imm16 { imm: 0x1234 });
428
+ assert!(validate_instruction(&instr).is_ok());
429
+ }
430
+
431
+ #[test]
432
+ fn validate_imm16_max_value() {
433
+ let instr = Instruction::new(Mnemonic::Trap, Operands::Imm16 { imm: u16::MAX });
434
+ assert!(validate_instruction(&instr).is_ok());
435
+ }
436
+
437
+ #[test]
438
+ fn error_message_includes_context() {
439
+ let instr = Instruction::new(
440
+ Mnemonic::Not,
441
+ Operands::R {
442
+ rd: Register::new(32),
443
+ },
444
+ );
445
+ let result = validate_instruction(&instr);
446
+ assert!(result.is_err());
447
+ let err_msg = result.unwrap_err().to_string();
448
+ assert!(err_msg.contains("invalid instruction") || err_msg.contains("register"));
449
+ }
450
+
451
+ #[test]
452
+ fn all_gp_registers_are_valid() {
453
+ for reg_idx in 0..32 {
454
+ let instr = Instruction::new(
455
+ Mnemonic::Not,
456
+ Operands::R {
457
+ rd: Register::new(reg_idx as u8),
458
+ },
459
+ );
460
+ assert!(
461
+ validate_instruction(&instr).is_ok(),
462
+ "register {} should be valid",
463
+ reg_idx
464
+ );
465
+ }
466
+ }
467
+
468
+ #[test]
469
+ fn boundary_register_32_is_invalid() {
470
+ let instr = Instruction::new(
471
+ Mnemonic::Not,
472
+ Operands::R {
473
+ rd: Register::new(32),
474
+ },
475
+ );
476
+ assert!(validate_instruction(&instr).is_err());
477
+ }
478
+
479
+ #[test]
480
+ fn boundary_register_255_is_invalid() {
481
+ let instr = Instruction::new(
482
+ Mnemonic::Not,
483
+ Operands::R {
484
+ rd: Register::new(255),
485
+ },
486
+ );
487
+ assert!(validate_instruction(&instr).is_err());
488
+ }
489
+ }
crates/jxcl-instructions/Cargo.toml ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [package]
2
+ name = "jxcl-instructions"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+ description = "The Instruction type tying an opcode to its operands -- the canonical in-memory instruction representation."
6
+ license = "AGPL-3.0-only OR LicenseRef-Commercial"
7
+
8
+ [dependencies]
9
+ jxcl-opcodes = { path = "../jxcl-opcodes" }
10
+ jxcl-operands = { path = "../jxcl-operands" }
11
+
12
+ [dev-dependencies]
13
+ jxcl-registers = { path = "../jxcl-registers" }
crates/jxcl-instructions/README.md ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # jxcl-instructions
2
+
3
+ The `Instruction` type tying an opcode to its operands -- the canonical
4
+ in-memory instruction representation.
5
+
6
+ ## Architecture
7
+
8
+ **Owns:** `DecodedInstruction` (aliased as `Instruction`): a mnemonic
9
+ paired with its decoded operands and the pre-computed encoded length.
10
+ This is both the encoder's input and the decoder's output.
11
+
12
+ Extracted from `jxcl/src/isa/instruction.rs`, unchanged in shape. This
13
+ crate is also the mechanical bridge between `jxcl-opcodes` and
14
+ `jxcl-operands` for the crates below it: `docs/crates.toml` gives
15
+ `jxcl-encoding`/`jxcl-decoding` a dependency list of
16
+ `jxcl-instructions`, `jxcl-bitops`, `jxcl-endian`, `jxcl-bytes`,
17
+ `jxcl-errors` -- deliberately not `jxcl-opcodes`/`jxcl-operands`
18
+ directly -- so this crate re-exports `Mnemonic` (from `jxcl-opcodes`)
19
+ and `Format`/`Operands`/`Register` (from `jxcl-operands`), and provides
20
+ the one piece of glue only possible here: `jxcl-opcodes::Format` (the
21
+ registry's per-opcode wire-format tag) and `jxcl-operands::Format` (an
22
+ operand set's actual shape) are two structurally identical but distinct
23
+ Rust types -- `jxcl-opcodes` cannot depend on `jxcl-operands` without
24
+ creating a cycle (see `jxcl-opcodes`'s module doc) -- so
25
+ `format_matches_registry`/`instruction_shape_for_opcode` are the
26
+ mechanical conversion between them.
27
+
28
+ **Category:** isa · **Source:** extraction:jxcl/src/isa/instruction.rs
29
+
30
+ ## Public API
31
+
32
+ - `DecodedInstruction` (= `Instruction`) `{ mnemonic, operands, length }`
33
+ — `DecodedInstruction::new(mnemonic, operands) -> Self`
34
+ - Re-exported: `Mnemonic`, `Format`, `Operands`, `Register`
35
+ - `opcode_byte(mnemonic) -> u8`
36
+ - `instruction_shape_for_opcode(opcode) -> Option<(Mnemonic, Format)>`
37
+ - `format_matches_registry(&DecodedInstruction) -> bool`
38
+
39
+ ## Dependencies
40
+
41
+ Workspace crates:
42
+
43
+ - `jxcl-opcodes`
44
+ - `jxcl-operands`
45
+
46
+ Dev-dependencies (tests only):
47
+
48
+ - `jxcl-registers` (to construct `Register` values for test operands)
49
+
50
+ External crates:
51
+
52
+ *(none)*
53
+
54
+ ## Examples
55
+
56
+ ```rust
57
+ use jxcl_instructions::{format_matches_registry, Instruction, Mnemonic, Operands, Register};
58
+
59
+ let instr = Instruction::new(
60
+ Mnemonic::Movi,
61
+ Operands::RImm64 { rd: Register::new(0), imm: 42 },
62
+ );
63
+ assert_eq!(instr.length, 10); // opcode + rd + imm64
64
+ assert!(format_matches_registry(&instr));
65
+
66
+ // Wrong operand shape for ADD (registered as RR) is caught mechanically:
67
+ let bad = Instruction::new(Mnemonic::Add, Operands::R { rd: Register::new(1) });
68
+ assert!(!format_matches_registry(&bad));
69
+ ```
70
+
71
+ ## Testing
72
+
73
+ `cargo test -p jxcl-instructions` — 10 unit tests: `length` derivation,
74
+ `length` matching `operands.format().len()` across all ten operand
75
+ shapes, the `Instruction` alias, field pass-through, `opcode_byte`
76
+ against known registry entries, `instruction_shape_for_opcode`
77
+ round-tripping (including the unassigned-opcode `None` case),
78
+ `format_matches_registry` accepting/rejecting shapes, and an exhaustive
79
+ round-trip of the opcode/operand `Format` bridge over every registered
80
+ instruction.
81
+
82
+ ## Status
83
+
84
+ `implemented`.
85
+
86
+ ## License
87
+
88
+ AGPLv3 / Commercial dual-license. See `LICENSE-AGPL`, `LICENSE-COMMERCIAL`, and `LICENSE-NOTICE` at the repository root.
crates/jxcl-instructions/src/lib.rs ADDED
@@ -0,0 +1,284 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Copyright (C) 2026 ERRANT DIGITAL INSTITUTE OF TECHNOLOGY (SEIT NGO) % SNAPKITTY COLLECTIVE
2
+ // SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial
3
+ // See LICENSE-AGPL and LICENSE-COMMERCIAL in this repository for license terms.
4
+
5
+ //! The `Instruction` type tying an opcode to its operands -- the
6
+ //! canonical in-memory instruction representation.
7
+ //!
8
+ //! Extracted from `jxcl/src/isa/instruction.rs` (there named
9
+ //! `DecodedInstruction`). This crate keeps the real name
10
+ //! `DecodedInstruction` as the primary type (every downstream extraction
11
+ //! crate in this batch, `jxcl-encoding`/`jxcl-decoding`, is written
12
+ //! against it) and additionally exposes `Instruction` as an alias,
13
+ //! matching `docs/crates.toml`'s literal `public_api`.
14
+ //!
15
+ //! Owns: the `Instruction`/`DecodedInstruction` struct -- the single
16
+ //! representation the encoder produces bytes from and the decoder
17
+ //! produces from bytes.
18
+ //!
19
+ //! ## Re-exports and the opcode/operand `Format` bridge
20
+ //!
21
+ //! `docs/crates.toml` gives `jxcl-encoding`/`jxcl-decoding` a dependency
22
+ //! list of `jxcl-instructions`, `jxcl-bitops`, `jxcl-endian`,
23
+ //! `jxcl-bytes`, `jxcl-errors` -- deliberately *not* `jxcl-opcodes` or
24
+ //! `jxcl-operands` directly. This crate therefore re-exports the pieces
25
+ //! of those two crates' public API that the encoder/decoder need
26
+ //! (`Mnemonic`, `Operands`/`Format`, `Register`) so they never need a
27
+ //! redundant direct dependency, and additionally provides the one piece
28
+ //! of glue logic only possible here: `jxcl-opcodes::Format` (the
29
+ //! registry's per-opcode wire-format tag) and `jxcl-operands::Format`
30
+ //! (the decoded operand values' actual shape) are two structurally
31
+ //! identical but distinct Rust types, defined in two crates that
32
+ //! deliberately don't depend on each other (see `jxcl-opcodes`'s module
33
+ //! doc for why: `jxcl-operands` depending on `jxcl-opcodes` the other
34
+ //! direction would create a cycle). [`format_matches_registry`] and
35
+ //! [`instruction_shape_for_opcode`] are the mechanical bridge between
36
+ //! them, used by the encoder to reject an operand shape that doesn't
37
+ //! match its mnemonic's registered format, and by the decoder to know
38
+ //! which operand shape to parse for a given opcode byte.
39
+ #![forbid(unsafe_code)]
40
+
41
+ pub use jxcl_opcodes::Mnemonic;
42
+ pub use jxcl_operands::{Format, Operands, Register};
43
+
44
+ /// A fully decoded (or, prior to encoding, fully specified) instruction.
45
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
46
+ pub struct DecodedInstruction {
47
+ pub mnemonic: Mnemonic,
48
+ pub operands: Operands,
49
+ /// Encoded length in bytes (redundant with `operands.format().len()`,
50
+ /// but carried explicitly so callers never need to recompute it --
51
+ /// spec §20's `DecodedInstruction { ... length ... }`).
52
+ pub length: usize,
53
+ }
54
+
55
+ impl DecodedInstruction {
56
+ pub fn new(mnemonic: Mnemonic, operands: Operands) -> Self {
57
+ DecodedInstruction {
58
+ mnemonic,
59
+ operands,
60
+ length: operands.format().len(),
61
+ }
62
+ }
63
+ }
64
+
65
+ /// Alias matching `docs/crates.toml`'s `public_api` naming
66
+ /// (`Instruction`); see the module-level doc comment.
67
+ pub type Instruction = DecodedInstruction;
68
+
69
+ /// A backend-agnostic tag for instruction wire-format shape: the common
70
+ /// ground between `jxcl-opcodes::Format` (the registry's per-entry wire
71
+ /// format) and `jxcl-operands::Format` (an operand set's actual shape).
72
+ /// See the module-level "Re-exports and the opcode/operand `Format`
73
+ /// bridge" doc for why two distinct types exist for the same ten shapes.
74
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
75
+ #[allow(clippy::upper_case_acronyms)]
76
+ enum FormatShape {
77
+ None,
78
+ R,
79
+ RR,
80
+ RRR,
81
+ RImm64,
82
+ RMem,
83
+ MemR,
84
+ Cas,
85
+ BranchImm32,
86
+ Imm16,
87
+ }
88
+
89
+ impl From<jxcl_opcodes::Format> for FormatShape {
90
+ fn from(f: jxcl_opcodes::Format) -> Self {
91
+ use jxcl_opcodes::Format as OF;
92
+ match f {
93
+ OF::None => FormatShape::None,
94
+ OF::R => FormatShape::R,
95
+ OF::RR => FormatShape::RR,
96
+ OF::RRR => FormatShape::RRR,
97
+ OF::RImm64 => FormatShape::RImm64,
98
+ OF::RMem => FormatShape::RMem,
99
+ OF::MemR => FormatShape::MemR,
100
+ OF::Cas => FormatShape::Cas,
101
+ OF::BranchImm32 => FormatShape::BranchImm32,
102
+ OF::Imm16 => FormatShape::Imm16,
103
+ }
104
+ }
105
+ }
106
+
107
+ impl From<Format> for FormatShape {
108
+ fn from(f: Format) -> Self {
109
+ match f {
110
+ Format::None => FormatShape::None,
111
+ Format::R => FormatShape::R,
112
+ Format::RR => FormatShape::RR,
113
+ Format::RRR => FormatShape::RRR,
114
+ Format::RImm64 => FormatShape::RImm64,
115
+ Format::RMem => FormatShape::RMem,
116
+ Format::MemR => FormatShape::MemR,
117
+ Format::Cas => FormatShape::Cas,
118
+ Format::BranchImm32 => FormatShape::BranchImm32,
119
+ Format::Imm16 => FormatShape::Imm16,
120
+ }
121
+ }
122
+ }
123
+
124
+ impl From<FormatShape> for Format {
125
+ fn from(s: FormatShape) -> Self {
126
+ match s {
127
+ FormatShape::None => Format::None,
128
+ FormatShape::R => Format::R,
129
+ FormatShape::RR => Format::RR,
130
+ FormatShape::RRR => Format::RRR,
131
+ FormatShape::RImm64 => Format::RImm64,
132
+ FormatShape::RMem => Format::RMem,
133
+ FormatShape::MemR => Format::MemR,
134
+ FormatShape::Cas => Format::Cas,
135
+ FormatShape::BranchImm32 => Format::BranchImm32,
136
+ FormatShape::Imm16 => Format::Imm16,
137
+ }
138
+ }
139
+ }
140
+
141
+ /// The opcode byte the registry assigns to `mnemonic` (spec §19). Used
142
+ /// by `jxcl-encoding` in place of a direct `jxcl-opcodes` dependency.
143
+ pub fn opcode_byte(mnemonic: Mnemonic) -> u8 {
144
+ jxcl_opcodes::lookup_mnemonic(mnemonic).opcode
145
+ }
146
+
147
+ /// The mnemonic and operand-value shape registered for `opcode`, or
148
+ /// `None` if no instruction is assigned that opcode. Used by
149
+ /// `jxcl-decoding` to know both what to name the decoded instruction
150
+ /// and which operand fields to parse next, in one registry lookup.
151
+ pub fn instruction_shape_for_opcode(opcode: u8) -> Option<(Mnemonic, Format)> {
152
+ jxcl_opcodes::lookup_opcode(opcode)
153
+ .map(|def| (def.mnemonic, FormatShape::from(def.format).into()))
154
+ }
155
+
156
+ /// True if `instr`'s operand shape matches what the opcode registry
157
+ /// declares for its mnemonic (spec §19: "avoid duplicated opcode
158
+ /// definitions across the assembler, decoder, and executor" -- this is
159
+ /// the mechanical check that a hand-built `DecodedInstruction` cannot
160
+ /// silently drift from the registry). Used by `jxcl-encoding` before
161
+ /// writing any bytes.
162
+ pub fn format_matches_registry(instr: &DecodedInstruction) -> bool {
163
+ let def = jxcl_opcodes::lookup_mnemonic(instr.mnemonic);
164
+ FormatShape::from(def.format) == FormatShape::from(instr.operands.format())
165
+ }
166
+
167
+ #[cfg(test)]
168
+ mod tests {
169
+ use super::*;
170
+
171
+ #[test]
172
+ fn new_computes_length_from_operands_format() {
173
+ let instr = DecodedInstruction::new(Mnemonic::Halt, Operands::None);
174
+ assert_eq!(instr.length, 1);
175
+
176
+ let instr = DecodedInstruction::new(
177
+ Mnemonic::Movi,
178
+ Operands::RImm64 {
179
+ rd: jxcl_registers::Register::new(0),
180
+ imm: 42,
181
+ },
182
+ );
183
+ assert_eq!(instr.length, 10);
184
+ }
185
+
186
+ #[test]
187
+ fn length_matches_operands_format_len_for_every_shape() {
188
+ let rd = jxcl_registers::Register::new(1);
189
+ let cases = [
190
+ Operands::None,
191
+ Operands::R { rd },
192
+ Operands::RR { rd, rs: rd },
193
+ Operands::RRR {
194
+ rd,
195
+ rs1: rd,
196
+ rs2: rd,
197
+ },
198
+ Operands::RImm64 { rd, imm: 0 },
199
+ Operands::RMem {
200
+ rd,
201
+ base: rd,
202
+ disp: 0,
203
+ },
204
+ Operands::MemR {
205
+ base: rd,
206
+ disp: 0,
207
+ rs: rd,
208
+ },
209
+ Operands::Cas {
210
+ rd,
211
+ base: rd,
212
+ rs_new: rd,
213
+ disp: 0,
214
+ },
215
+ Operands::BranchImm32 { disp: 0 },
216
+ Operands::Imm16 { imm: 0 },
217
+ ];
218
+ for ops in cases {
219
+ let instr = DecodedInstruction::new(Mnemonic::Nop, ops);
220
+ assert_eq!(instr.length, ops.format().len());
221
+ }
222
+ }
223
+
224
+ #[test]
225
+ fn instruction_alias_is_the_real_type() {
226
+ let instr: Instruction = DecodedInstruction::new(Mnemonic::Ret, Operands::None);
227
+ assert_eq!(instr.mnemonic, Mnemonic::Ret);
228
+ }
229
+
230
+ #[test]
231
+ fn instructions_carry_mnemonic_and_operands_unchanged() {
232
+ let rd = jxcl_registers::Register::new(3);
233
+ let ops = Operands::R { rd };
234
+ let instr = DecodedInstruction::new(Mnemonic::Push, ops);
235
+ assert_eq!(instr.mnemonic, Mnemonic::Push);
236
+ assert_eq!(instr.operands, ops);
237
+ }
238
+
239
+ #[test]
240
+ fn opcode_byte_matches_the_registry() {
241
+ assert_eq!(opcode_byte(Mnemonic::Halt), 0x60);
242
+ assert_eq!(opcode_byte(Mnemonic::Add), 0x10);
243
+ }
244
+
245
+ #[test]
246
+ fn instruction_shape_for_opcode_roundtrips_with_opcode_byte() {
247
+ let (mnemonic, format) = instruction_shape_for_opcode(0x10).unwrap();
248
+ assert_eq!(mnemonic, Mnemonic::Add);
249
+ assert_eq!(format, Format::RR);
250
+ }
251
+
252
+ #[test]
253
+ fn instruction_shape_for_unassigned_opcode_is_none() {
254
+ assert!(instruction_shape_for_opcode(0xFF).is_none());
255
+ }
256
+
257
+ #[test]
258
+ fn format_matches_registry_accepts_correctly_shaped_instruction() {
259
+ let rd = Register::new(1);
260
+ let instr = DecodedInstruction::new(Mnemonic::Add, Operands::RR { rd, rs: rd });
261
+ assert!(format_matches_registry(&instr));
262
+ }
263
+
264
+ #[test]
265
+ fn format_matches_registry_rejects_wrong_shape() {
266
+ // ADD is registered as RR, not R.
267
+ let instr = DecodedInstruction::new(
268
+ Mnemonic::Add,
269
+ Operands::R {
270
+ rd: Register::new(1),
271
+ },
272
+ );
273
+ assert!(!format_matches_registry(&instr));
274
+ }
275
+
276
+ #[test]
277
+ fn every_registry_entry_shape_round_trips_through_the_bridge() {
278
+ for def in jxcl_opcodes::all_defs() {
279
+ let (mnemonic, format) = instruction_shape_for_opcode(def.opcode).unwrap();
280
+ assert_eq!(mnemonic, def.mnemonic);
281
+ assert_eq!(FormatShape::from(format), FormatShape::from(def.format));
282
+ }
283
+ }
284
+ }
crates/jxcl-integration/Cargo.toml ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [package]
2
+ name = "jxcl-integration"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+ description = "An integration-test-only crate exercising the full stack end-to-end: assemble, run in jxcl-simulator, seal/store in pq-cache, attest with pq-error-proof."
6
+ license = "AGPL-3.0-only OR LicenseRef-Commercial"
7
+
8
+ [dependencies]
9
+ jxcl-assembler = { path = "../jxcl-assembler" }
10
+ jxcl-simulator = { path = "../jxcl-simulator" }
11
+ pq-cache = { path = "../pq-cache" }
12
+ pq-error-proof = { path = "../pq-error-proof" }
crates/jxcl-integration/README.md ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # jxcl-integration
2
+
3
+ An integration-test-only crate exercising the full stack end-to-end: assemble, run in jxcl-simulator, seal/store in pq-cache, attest with pq-error-proof.
4
+
5
+ ## Architecture
6
+
7
+ **Owns:** The canonical whole-workspace integration tests.
8
+
9
+ **Category:** security · **Source:** new
10
+
11
+ ## Public API
12
+
13
+ `(test-only crate)`
14
+
15
+ ## Dependencies
16
+
17
+ Workspace crates:
18
+
19
+ - `jxcl-assembler`
20
+ - `jxcl-simulator`
21
+ - `pq-cache`
22
+ - `pq-error-proof`
23
+
24
+ External crates:
25
+
26
+ *(none)*
27
+
28
+ ## Testing
29
+
30
+ Planned test kinds: integration.
31
+
32
+ ## Status
33
+
34
+ `planned` in `docs/crates.toml` -- see
35
+ `docs/CRATE_GENERATION_PLAN.md` for the implementation batch schedule.
36
+
37
+ ## License
38
+
39
+ AGPLv3 / Commercial dual-license. See `LICENSE-AGPL`, `LICENSE-COMMERCIAL`, and `LICENSE-NOTICE` at the repository root.
crates/jxcl-integration/src/lib.rs ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Copyright (C) 2026 ERRANT DIGITAL INSTITUTE OF TECHNOLOGY (SEIT NGO) % SNAPKITTY COLLECTIVE
2
+ // SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial
3
+ // See LICENSE-AGPL and LICENSE-COMMERCIAL in this repository for license terms.
4
+
5
+ //! An integration-test-only crate exercising the full stack end-to-end: assemble, run in jxcl-simulator, seal/store in pq-cache, attest with pq-error-proof.
6
+ //!
7
+ //! Owns: The canonical whole-workspace integration tests.
8
+ //!
9
+ //! Status: scaffolded -- real implementation lands per
10
+ //! `docs/CRATE_GENERATION_PLAN.md`'s batch schedule for the
11
+ //! `security` category. Planned public API: (test-only crate).
12
+ #![forbid(unsafe_code)]
13
+ #![allow(dead_code)]
14
+
15
+ #[cfg(test)]
16
+ mod tests {
17
+ #[test]
18
+ fn scaffold_placeholder() {
19
+ // Real tests land with this crate's implementation batch --
20
+ // see docs/crates.toml's `tests` field for what's planned:
21
+ // integration.
22
+ }
23
+ }
crates/jxcl-interrupts/Cargo.toml ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ [package]
2
+ name = "jxcl-interrupts"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+ description = "A deterministic external-interrupt injection mechanism (priority queue + mask flag) for embedding jxcl in a simulator or RPC host."
6
+ license = "AGPL-3.0-only OR LicenseRef-Commercial"
7
+
8
+ [dependencies]
9
+ jxcl-flags = { path = "../jxcl-flags" }
10
+ jxcl-exceptions = { path = "../jxcl-exceptions" }
crates/jxcl-interrupts/README.md ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # jxcl-interrupts
2
+
3
+ A deterministic external-interrupt injection mechanism (priority queue + mask flag) for embedding jxcl in a simulator or RPC host.
4
+
5
+ ## Architecture
6
+
7
+ **Owns:** InterruptController: queue/mask/deliver.
8
+
9
+ **Category:** execution · **Source:** new
10
+
11
+ ## Public API
12
+
13
+ `InterruptController`
14
+
15
+ ## Dependencies
16
+
17
+ Workspace crates:
18
+
19
+ - `jxcl-flags`
20
+ - `jxcl-exceptions`
21
+
22
+ External crates:
23
+
24
+ *(none)*
25
+
26
+ ## Testing
27
+
28
+ Planned test kinds: unit, determinism.
29
+
30
+ ## Status
31
+
32
+ `planned` in `docs/crates.toml` -- see
33
+ `docs/CRATE_GENERATION_PLAN.md` for the implementation batch schedule.
34
+
35
+ ## License
36
+
37
+ AGPLv3 / Commercial dual-license. See `LICENSE-AGPL`, `LICENSE-COMMERCIAL`, and `LICENSE-NOTICE` at the repository root.
crates/jxcl-interrupts/src/lib.rs ADDED
@@ -0,0 +1,203 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Copyright (C) 2026 ERRANT DIGITAL INSTITUTE OF TECHNOLOGY (SEIT NGO) % SNAPKITTY COLLECTIVE
2
+ // SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial
3
+ // See LICENSE-AGPL and LICENSE-COMMERCIAL in this repository for license terms.
4
+
5
+ //! A deterministic external-interrupt injection mechanism (priority queue + mask flag) for embedding jxcl in a simulator or RPC host.
6
+ //!
7
+ //! Owns: InterruptController: queue/mask/deliver. Provides a priority-based interrupt
8
+ //! queue that can be checked at each instruction boundary to determine if any
9
+ //! interrupts should be delivered to the machine.
10
+ #![forbid(unsafe_code)]
11
+
12
+ use std::collections::VecDeque;
13
+
14
+ /// An interrupt with a priority level and identifier.
15
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
16
+ pub struct Interrupt {
17
+ pub priority: u8,
18
+ pub id: u16,
19
+ }
20
+
21
+ impl Interrupt {
22
+ /// Create a new interrupt with the given priority and id.
23
+ pub const fn new(priority: u8, id: u16) -> Self {
24
+ Interrupt { priority, id }
25
+ }
26
+ }
27
+
28
+ /// Manages external interrupts for the machine: queuing, masking, and delivery.
29
+ /// Interrupts are processed in priority order (higher priority first).
30
+ #[derive(Debug, Clone)]
31
+ pub struct InterruptController {
32
+ queue: VecDeque<Interrupt>,
33
+ mask: bool,
34
+ }
35
+
36
+ impl InterruptController {
37
+ /// Create a new interrupt controller with the queue enabled (mask = false).
38
+ pub fn new() -> Self {
39
+ InterruptController {
40
+ queue: VecDeque::new(),
41
+ mask: false,
42
+ }
43
+ }
44
+
45
+ /// Inject an interrupt into the queue.
46
+ pub fn inject(&mut self, interrupt: Interrupt) {
47
+ self.queue.push_back(interrupt);
48
+ // Keep the queue sorted by priority (higher priority first)
49
+ self.sort_queue();
50
+ }
51
+
52
+ /// Check if the interrupt queue is empty.
53
+ pub fn is_empty(&self) -> bool {
54
+ self.queue.is_empty()
55
+ }
56
+
57
+ /// Get the number of pending interrupts.
58
+ pub fn pending_count(&self) -> usize {
59
+ self.queue.len()
60
+ }
61
+
62
+ /// Deliver the next interrupt if one is available and interrupts are enabled.
63
+ /// Returns Some(interrupt) if delivery occurred, None otherwise.
64
+ pub fn deliver(&mut self) -> Option<Interrupt> {
65
+ if self.mask || self.queue.is_empty() {
66
+ return None;
67
+ }
68
+ self.queue.pop_front()
69
+ }
70
+
71
+ /// Enable the interrupt mask (disable interrupt delivery).
72
+ pub fn set_mask(&mut self, masked: bool) {
73
+ self.mask = masked;
74
+ }
75
+
76
+ /// Check if interrupts are currently masked.
77
+ pub fn is_masked(&self) -> bool {
78
+ self.mask
79
+ }
80
+
81
+ /// Clear all pending interrupts.
82
+ pub fn clear(&mut self) {
83
+ self.queue.clear();
84
+ }
85
+
86
+ /// Sort the queue by priority (higher priority first).
87
+ fn sort_queue(&mut self) {
88
+ // For a simple VecDeque, we'll use a stable sort approach
89
+ // Convert to a vector, sort, and rebuild
90
+ let mut vec: Vec<_> = self.queue.iter().copied().collect();
91
+ vec.sort_by_key(|a| std::cmp::Reverse(a.priority)); // Higher priority first
92
+ self.queue = vec.into_iter().collect();
93
+ }
94
+ }
95
+
96
+ impl Default for InterruptController {
97
+ fn default() -> Self {
98
+ Self::new()
99
+ }
100
+ }
101
+
102
+ #[cfg(test)]
103
+ mod tests {
104
+ use super::*;
105
+
106
+ #[test]
107
+ fn interrupt_creation() {
108
+ let intr = Interrupt::new(5, 10);
109
+ assert_eq!(intr.priority, 5);
110
+ assert_eq!(intr.id, 10);
111
+ }
112
+
113
+ #[test]
114
+ fn interrupt_ordering() {
115
+ let intr1 = Interrupt::new(1, 100);
116
+ let intr2 = Interrupt::new(5, 200);
117
+ assert!(intr2 > intr1);
118
+ }
119
+
120
+ #[test]
121
+ fn interrupt_controller_empty() {
122
+ let controller = InterruptController::new();
123
+ assert!(controller.is_empty());
124
+ assert_eq!(controller.pending_count(), 0);
125
+ }
126
+
127
+ #[test]
128
+ fn interrupt_injection() {
129
+ let mut controller = InterruptController::new();
130
+ assert!(controller.is_empty());
131
+
132
+ controller.inject(Interrupt::new(5, 100));
133
+ assert!(!controller.is_empty());
134
+ assert_eq!(controller.pending_count(), 1);
135
+ }
136
+
137
+ #[test]
138
+ fn interrupt_delivery() {
139
+ let mut controller = InterruptController::new();
140
+ controller.inject(Interrupt::new(5, 100));
141
+
142
+ let intr = controller.deliver();
143
+ assert_eq!(intr, Some(Interrupt::new(5, 100)));
144
+ assert!(controller.is_empty());
145
+ }
146
+
147
+ #[test]
148
+ fn interrupt_masking() {
149
+ let mut controller = InterruptController::new();
150
+ assert!(!controller.is_masked());
151
+
152
+ controller.set_mask(true);
153
+ assert!(controller.is_masked());
154
+
155
+ controller.inject(Interrupt::new(5, 100));
156
+ // Should not deliver while masked
157
+ assert!(controller.deliver().is_none());
158
+
159
+ controller.set_mask(false);
160
+ // Now should deliver
161
+ assert_eq!(controller.deliver(), Some(Interrupt::new(5, 100)));
162
+ }
163
+
164
+ #[test]
165
+ fn interrupt_priority_ordering() {
166
+ let mut controller = InterruptController::new();
167
+ controller.inject(Interrupt::new(1, 100));
168
+ controller.inject(Interrupt::new(10, 200));
169
+ controller.inject(Interrupt::new(5, 300));
170
+
171
+ // Should deliver in priority order (highest first)
172
+ assert_eq!(controller.deliver().map(|i| i.priority), Some(10));
173
+ assert_eq!(controller.deliver().map(|i| i.priority), Some(5));
174
+ assert_eq!(controller.deliver().map(|i| i.priority), Some(1));
175
+ }
176
+
177
+ #[test]
178
+ fn interrupt_controller_clear() {
179
+ let mut controller = InterruptController::new();
180
+ controller.inject(Interrupt::new(5, 100));
181
+ controller.inject(Interrupt::new(3, 200));
182
+ assert_eq!(controller.pending_count(), 2);
183
+
184
+ controller.clear();
185
+ assert_eq!(controller.pending_count(), 0);
186
+ assert!(controller.is_empty());
187
+ }
188
+
189
+ #[test]
190
+ fn interrupt_controller_determinism() {
191
+ let mut controller1 = InterruptController::new();
192
+ let mut controller2 = InterruptController::new();
193
+
194
+ for i in 0..10 {
195
+ controller1.inject(Interrupt::new(i, i as u16));
196
+ controller2.inject(Interrupt::new(i, i as u16));
197
+ }
198
+
199
+ while let (Some(i1), Some(i2)) = (controller1.deliver(), controller2.deliver()) {
200
+ assert_eq!(i1, i2);
201
+ }
202
+ }
203
+ }
crates/jxcl-isa-metadata/Cargo.toml ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ [package]
2
+ name = "jxcl-isa-metadata"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+ description = "Queryable descriptive metadata about the running ISA build (opcode count, register count, build flags) used by `jxcl inspect` and the debugger."
6
+ license = "AGPL-3.0-only OR LicenseRef-Commercial"
7
+
8
+ [dependencies]
9
+ jxcl-constants = { path = "../jxcl-constants" }
10
+ jxcl-opcodes = { path = "../jxcl-opcodes" }
crates/jxcl-isa-metadata/README.md ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # jxcl-isa-metadata
2
+
3
+ Queryable descriptive metadata about the JXCL ISA build.
4
+
5
+ ## Purpose
6
+
7
+ Provides comprehensive, queryable metadata about the running ISA:
8
+ - Architectural parameters (word width, address width, opcode width)
9
+ - Register count and special register names
10
+ - Opcode statistics (assigned opcodes vs. total space)
11
+ - Architecture identifier and binary format version
12
+
13
+ Used by inspection tools (e.g., `jxcl inspect`) and debuggers to report ISA capabilities.
14
+
15
+ ## Public API
16
+
17
+ - `IsaMetadata::describe() -> IsaMetadata` - Generate metadata from live ISA registries and constants
18
+ - `IsaMetadata::validate() -> Result<(), String>` - Verify internal consistency
19
+ - `IsaMetadata::summary() -> String` - Human-readable summary
20
+ - `describe()` - Convenience function equivalent to `IsaMetadata::describe()`
21
+
22
+ ## Implementation Notes
23
+
24
+ - Metadata is generated entirely from the authoritative `jxcl-constants` and `jxcl-opcodes` registries
25
+ - The `validate()` method checks consistency (e.g., word width == address width == 64 bits)
26
+ - Opcode space is derived from opcode width via `2^opcode_width`
27
+ - Assigned opcodes count is a live query of the registry
28
+
29
+ ## Testing
30
+
31
+ The crate includes 9 unit tests covering:
32
+ - Metadata describes the live ISA correctly
33
+ - Self-consistency validation passes
34
+ - Opcode space calculations are correct
35
+ - Assigned opcodes match the registry
36
+ - Validation catches inconsistencies
37
+ - Display formatting works correctly
38
+
39
+ ## License
40
+
41
+ AGPLv3 / Commercial dual-license. See `LICENSE-AGPL`, `LICENSE-COMMERCIAL`, and `LICENSE-NOTICE` at the repository root.
crates/jxcl-isa-metadata/src/lib.rs ADDED
@@ -0,0 +1,231 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Copyright (C) 2026 ERRANT DIGITAL INSTITUTE OF TECHNOLOGY (SEIT NGO) % SNAPKITTY COLLECTIVE
2
+ // SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial
3
+ // See LICENSE-AGPL and LICENSE-COMMERCIAL in this repository for license terms.
4
+
5
+ //! Queryable descriptive metadata about the running ISA build (opcode count, register count, build flags) used by `jxcl inspect` and the debugger.
6
+ //!
7
+ //! Owns: `IsaMetadata` struct and the `describe()` function that
8
+ //! generates it from the authoritative ISA registries.
9
+ #![forbid(unsafe_code)]
10
+
11
+ use jxcl_constants::{
12
+ ADDRESS_WIDTH_BITS, ARCHITECTURE_ID, BINARY_VERSION, OPCODE_BITS, OPCODE_SPACE, REGISTER_COUNT,
13
+ REGISTER_WIDTH_BITS, WORD_BITS,
14
+ };
15
+ use jxcl_opcodes::assigned_opcode_count;
16
+
17
+ #[cfg(test)]
18
+ use jxcl_opcodes::all_defs;
19
+
20
+ /// Comprehensive queryable metadata about the running ISA build, used
21
+ /// by inspection tools, debuggers, and documentation generation.
22
+ ///
23
+ /// This aggregates architectural constants and registry statistics into
24
+ /// one authoritative description of the ISA's shape and capabilities.
25
+ #[derive(Debug, Clone, PartialEq, Eq)]
26
+ pub struct IsaMetadata {
27
+ /// Width of an architectural word, in bits.
28
+ pub word_width_bits: u32,
29
+ /// Width of an address, in bits.
30
+ pub address_width_bits: u32,
31
+ /// Width of the opcode field, in bits.
32
+ pub opcode_width_bits: u32,
33
+ /// Total number of addressable opcode values (2^opcode_width_bits).
34
+ pub opcode_space: usize,
35
+ /// Number of opcodes actually assigned in the registry.
36
+ pub assigned_opcodes: usize,
37
+ /// Number of general-purpose registers.
38
+ pub register_count: usize,
39
+ /// Architecture identifier (e.g., "JX" for JXCL).
40
+ pub architecture_id: u16,
41
+ /// Binary format version.
42
+ pub binary_version: u16,
43
+ /// Register width (should match word_width_bits).
44
+ pub register_width_bits: u32,
45
+ }
46
+
47
+ impl IsaMetadata {
48
+ /// Generates the authoritative ISA metadata from the live
49
+ /// architecture constants and opcode registry.
50
+ pub fn describe() -> Self {
51
+ IsaMetadata {
52
+ word_width_bits: WORD_BITS,
53
+ address_width_bits: ADDRESS_WIDTH_BITS,
54
+ opcode_width_bits: OPCODE_BITS,
55
+ opcode_space: OPCODE_SPACE,
56
+ assigned_opcodes: assigned_opcode_count(),
57
+ register_count: REGISTER_COUNT,
58
+ architecture_id: ARCHITECTURE_ID,
59
+ binary_version: BINARY_VERSION,
60
+ register_width_bits: REGISTER_WIDTH_BITS,
61
+ }
62
+ }
63
+
64
+ /// Check internal consistency of the metadata (word width and
65
+ /// address width match, opcode space is correct, etc.).
66
+ pub fn validate(&self) -> Result<(), String> {
67
+ // Word and address widths should be 64 bits for JXCL
68
+ if self.word_width_bits != 64 {
69
+ return Err(format!(
70
+ "word width mismatch: expected 64, got {}",
71
+ self.word_width_bits
72
+ ));
73
+ }
74
+ if self.address_width_bits != 64 {
75
+ return Err(format!(
76
+ "address width mismatch: expected 64, got {}",
77
+ self.address_width_bits
78
+ ));
79
+ }
80
+
81
+ // Opcode space should be 2^opcode_width_bits
82
+ let expected_space = 1usize << self.opcode_width_bits;
83
+ if self.opcode_space != expected_space {
84
+ return Err(format!(
85
+ "opcode space mismatch: expected {}, got {}",
86
+ expected_space, self.opcode_space
87
+ ));
88
+ }
89
+
90
+ // Assigned opcodes should not exceed the opcode space
91
+ if self.assigned_opcodes > self.opcode_space {
92
+ return Err(format!(
93
+ "assigned opcodes ({}) exceeds opcode space ({})",
94
+ self.assigned_opcodes, self.opcode_space
95
+ ));
96
+ }
97
+
98
+ // Register count should match the constant
99
+ if self.register_count != REGISTER_COUNT {
100
+ return Err(format!(
101
+ "register count mismatch: expected {}, got {}",
102
+ REGISTER_COUNT, self.register_count
103
+ ));
104
+ }
105
+
106
+ // Register width should match word width
107
+ if self.register_width_bits != self.word_width_bits {
108
+ return Err(format!(
109
+ "register width {} does not match word width {}",
110
+ self.register_width_bits, self.word_width_bits
111
+ ));
112
+ }
113
+
114
+ Ok(())
115
+ }
116
+
117
+ /// Get a human-readable summary of the ISA.
118
+ pub fn summary(&self) -> String {
119
+ format!(
120
+ "JXCL ISA: {}-bit words, {} registers, {} assigned opcodes (out of {}), version {}.{}",
121
+ self.word_width_bits,
122
+ self.register_count,
123
+ self.assigned_opcodes,
124
+ self.opcode_space,
125
+ self.architecture_id >> 8,
126
+ self.architecture_id & 0xFF
127
+ )
128
+ }
129
+ }
130
+
131
+ impl std::fmt::Display for IsaMetadata {
132
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
133
+ write!(
134
+ f,
135
+ "ISA Metadata:\n Word width: {} bits\n Address width: {} bits\n \
136
+ Opcode width: {} bits\n Opcode space: {} (assigned: {})\n \
137
+ Register count: {}\n Architecture ID: {:#06x}\n Binary version: {}",
138
+ self.word_width_bits,
139
+ self.address_width_bits,
140
+ self.opcode_width_bits,
141
+ self.opcode_space,
142
+ self.assigned_opcodes,
143
+ self.register_count,
144
+ self.architecture_id,
145
+ self.binary_version
146
+ )
147
+ }
148
+ }
149
+
150
+ /// Convenience function that generates and returns the authoritative ISA
151
+ /// metadata. Equivalent to `IsaMetadata::describe()`.
152
+ pub fn describe() -> IsaMetadata {
153
+ IsaMetadata::describe()
154
+ }
155
+
156
+ #[cfg(test)]
157
+ mod tests {
158
+ use super::*;
159
+
160
+ #[test]
161
+ fn metadata_describes_live_isa() {
162
+ let meta = IsaMetadata::describe();
163
+ assert_eq!(meta.word_width_bits, 64);
164
+ assert_eq!(meta.address_width_bits, 64);
165
+ assert_eq!(meta.opcode_width_bits, 8);
166
+ assert_eq!(meta.register_count, 32);
167
+ }
168
+
169
+ #[test]
170
+ fn metadata_is_self_consistent() {
171
+ let meta = IsaMetadata::describe();
172
+ assert!(meta.validate().is_ok());
173
+ }
174
+
175
+ #[test]
176
+ fn opcode_space_is_correct() {
177
+ let meta = IsaMetadata::describe();
178
+ assert_eq!(meta.opcode_space, 256); // 2^8
179
+ assert!(meta.assigned_opcodes <= meta.opcode_space);
180
+ }
181
+
182
+ #[test]
183
+ fn assigned_opcodes_matches_registry() {
184
+ let meta = IsaMetadata::describe();
185
+ let registry_count = all_defs().len();
186
+ assert_eq!(meta.assigned_opcodes, registry_count);
187
+ }
188
+
189
+ #[test]
190
+ fn metadata_validation_catches_inconsistencies() {
191
+ let mut bad_meta = IsaMetadata::describe();
192
+ bad_meta.word_width_bits = 32; // Inconsistent!
193
+ assert!(bad_meta.validate().is_err());
194
+
195
+ let mut bad_meta2 = IsaMetadata::describe();
196
+ bad_meta2.assigned_opcodes = bad_meta2.opcode_space + 1; // Exceeds space
197
+ assert!(bad_meta2.validate().is_err());
198
+ }
199
+
200
+ #[test]
201
+ fn summary_formatting() {
202
+ let meta = IsaMetadata::describe();
203
+ let summary = meta.summary();
204
+ assert!(summary.contains("64-bit"));
205
+ assert!(summary.contains("32 registers"));
206
+ assert!(summary.contains("opcodes"));
207
+ }
208
+
209
+ #[test]
210
+ fn display_formatting() {
211
+ let meta = IsaMetadata::describe();
212
+ let display = meta.to_string();
213
+ assert!(display.contains("ISA Metadata"));
214
+ assert!(display.contains("Word width"));
215
+ assert!(display.contains("Register count"));
216
+ }
217
+
218
+ #[test]
219
+ fn boundary_register_count() {
220
+ let meta = IsaMetadata::describe();
221
+ assert!(meta.register_count > 0);
222
+ assert_eq!(meta.register_count, REGISTER_COUNT);
223
+ }
224
+
225
+ #[test]
226
+ fn architecture_id_is_nonzero() {
227
+ let meta = IsaMetadata::describe();
228
+ assert_ne!(meta.architecture_id, 0);
229
+ assert_eq!(meta.architecture_id, ARCHITECTURE_ID);
230
+ }
231
+ }
crates/jxcl-isa-schema/Cargo.toml ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [package]
2
+ name = "jxcl-isa-schema"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+ description = "A serde-serializable schema of the ISA generated from jxcl-opcodes/jxcl-constants/jxcl-registers/jxcl-flags, plus a mechanical cross-check against the numbers documented in docs/ISA_SPEC.md."
6
+ license = "AGPL-3.0-only OR LicenseRef-Commercial"
7
+
8
+ [dependencies]
9
+ jxcl-opcodes = { path = "../jxcl-opcodes" }
10
+ jxcl-constants = { path = "../jxcl-constants" }
11
+ jxcl-registers = { path = "../jxcl-registers" }
12
+ jxcl-flags = { path = "../jxcl-flags" }
13
+ serde = { version = "1.0", features = ["derive"] }
14
+ serde_json = "1.0"
crates/jxcl-isa-schema/README.md ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # jxcl-isa-schema
2
+
3
+ A serde-serializable schema of the JXCL ISA with conformance checking against documentation.
4
+
5
+ ## Purpose
6
+
7
+ Provides a machine-readable, serde-serializable schema of the JXCL ISA:
8
+ - Architectural parameters (word width, address width, opcode width, binary version)
9
+ - Opcode table metadata (addressable space, assigned opcodes)
10
+ - Register file description (GP register count, special registers)
11
+ - Flags register description (flag names)
12
+
13
+ Can be checked against documentation to ensure spec/code consistency.
14
+
15
+ ## Public API
16
+
17
+ - `IsaSchema::generate() -> IsaSchema` - Generate schema from live ISA registries
18
+ - `IsaSchema::check_against_spec() -> Result<(), String>` - Verify against documented ISA parameters
19
+ - `IsaSchema::to_json(&self) -> Result<String>` - Serialize to JSON
20
+ - `IsaSchema::from_json(json: &str) -> Result<IsaSchema>` - Deserialize from JSON
21
+
22
+ ## Implementation Notes
23
+
24
+ - The schema is mechanical - generated entirely from live authoritative sources
25
+ - Documented values (64-bit words, 32 registers, 256 opcode space, etc.) are embedded as invariants
26
+ - `check_against_spec()` verifies the documented ISA parameters match the code
27
+ - Full JSON round-trip compatibility via serde
28
+
29
+ ## Testing
30
+
31
+ The crate includes 15 unit tests covering:
32
+ - Schema generation from live ISA
33
+ - Spec conformance checking
34
+ - Detection of architectural parameter mismatches
35
+ - JSON serialization and deserialization
36
+ - Schema round-trip preservation
37
+ - Opcode space calculations and sanity checks
38
+
39
+ ## License
40
+
41
+ AGPLv3 / Commercial dual-license. See `LICENSE-AGPL`, `LICENSE-COMMERCIAL`, and `LICENSE-NOTICE` at the repository root.
crates/jxcl-isa-schema/src/lib.rs ADDED
@@ -0,0 +1,306 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Copyright (C) 2026 ERRANT DIGITAL INSTITUTE OF TECHNOLOGY (SEIT NGO) % SNAPKITTY COLLECTIVE
2
+ // SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial
3
+ // See LICENSE-AGPL and LICENSE-COMMERCIAL in this repository for license terms.
4
+
5
+ //! A serde-serializable schema of the ISA generated from jxcl-opcodes/jxcl-constants/jxcl-registers/jxcl-flags, plus a mechanical cross-check against the numbers documented in docs/ISA_SPEC.md.
6
+ //!
7
+ //! Owns: `IsaSchema` struct and the `check_against_spec()` conformance
8
+ //! check that verifies the documented ISA parameters match the live code.
9
+ #![forbid(unsafe_code)]
10
+
11
+ use serde::{Deserialize, Serialize};
12
+
13
+ use jxcl_constants::{
14
+ ADDRESS_WIDTH_BITS, BINARY_VERSION, OPCODE_BITS, OPCODE_SPACE, REGISTER_COUNT, WORD_BITS,
15
+ };
16
+ use jxcl_opcodes::all_defs;
17
+
18
+ /// A complete, serde-serializable schema of the JXCL ISA, capturing
19
+ /// architectural parameters and opcode table metadata.
20
+ ///
21
+ /// This schema is mechanical - generated entirely from the live
22
+ /// authoritative ISA registries and constants - and can be checked against
23
+ /// documentation to ensure spec/code consistency (see `check_against_spec`).
24
+ #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
25
+ pub struct IsaSchema {
26
+ /// Architectural parameters
27
+ pub architecture: ArchitectureSchema,
28
+ /// Opcode table
29
+ pub opcodes: OpcodeTableSchema,
30
+ /// Register file description
31
+ pub registers: RegisterSchema,
32
+ /// Flags register description
33
+ pub flags: FlagsSchema,
34
+ }
35
+
36
+ #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
37
+ pub struct ArchitectureSchema {
38
+ /// Width of data words, in bits.
39
+ pub word_width_bits: u32,
40
+ /// Width of addresses, in bits.
41
+ pub address_width_bits: u32,
42
+ /// Width of opcode field, in bits.
43
+ pub opcode_width_bits: u32,
44
+ /// Binary format version.
45
+ pub binary_version: u16,
46
+ }
47
+
48
+ #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
49
+ pub struct OpcodeTableSchema {
50
+ /// Total addressable opcode space.
51
+ pub space: usize,
52
+ /// Number of opcodes actually assigned.
53
+ pub assigned: usize,
54
+ }
55
+
56
+ #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
57
+ pub struct RegisterSchema {
58
+ /// Number of general-purpose registers.
59
+ pub gp_count: usize,
60
+ /// Special registers: PC, SP, FP, FLAGS.
61
+ pub special_registers: Vec<String>,
62
+ }
63
+
64
+ #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
65
+ pub struct FlagsSchema {
66
+ /// Named flags in the flags register.
67
+ pub flags: Vec<String>,
68
+ }
69
+
70
+ impl IsaSchema {
71
+ /// Generate the authoritative ISA schema from live constants and registries.
72
+ pub fn generate() -> Self {
73
+ IsaSchema {
74
+ architecture: ArchitectureSchema {
75
+ word_width_bits: WORD_BITS,
76
+ address_width_bits: ADDRESS_WIDTH_BITS,
77
+ opcode_width_bits: OPCODE_BITS,
78
+ binary_version: BINARY_VERSION,
79
+ },
80
+ opcodes: OpcodeTableSchema {
81
+ space: OPCODE_SPACE,
82
+ assigned: all_defs().len(),
83
+ },
84
+ registers: RegisterSchema {
85
+ gp_count: REGISTER_COUNT,
86
+ special_registers: vec![
87
+ "PC".to_string(),
88
+ "SP".to_string(),
89
+ "FP".to_string(),
90
+ "FLAGS".to_string(),
91
+ ],
92
+ },
93
+ flags: FlagsSchema {
94
+ flags: vec![
95
+ "Z (zero)".to_string(),
96
+ "N (negative)".to_string(),
97
+ "C (carry)".to_string(),
98
+ "V (overflow)".to_string(),
99
+ ],
100
+ },
101
+ }
102
+ }
103
+
104
+ /// Check the schema against documented ISA parameters (spec conformance).
105
+ ///
106
+ /// This verifies:
107
+ /// - Word width is 64 bits (as documented)
108
+ /// - Address width is 64 bits (as documented)
109
+ /// - Opcode width is 8 bits (as documented)
110
+ /// - Register count is 32 (as documented)
111
+ /// - Opcode space is 256 (as documented)
112
+ pub fn check_against_spec(&self) -> Result<(), String> {
113
+ // Check documented architectural parameters
114
+ if self.architecture.word_width_bits != 64 {
115
+ return Err(format!(
116
+ "word width: spec says 64, code has {}",
117
+ self.architecture.word_width_bits
118
+ ));
119
+ }
120
+
121
+ if self.architecture.address_width_bits != 64 {
122
+ return Err(format!(
123
+ "address width: spec says 64, code has {}",
124
+ self.architecture.address_width_bits
125
+ ));
126
+ }
127
+
128
+ if self.architecture.opcode_width_bits != 8 {
129
+ return Err(format!(
130
+ "opcode width: spec says 8, code has {}",
131
+ self.architecture.opcode_width_bits
132
+ ));
133
+ }
134
+
135
+ // Check opcode space consistency
136
+ let expected_space = 1usize << self.architecture.opcode_width_bits;
137
+ if self.opcodes.space != expected_space {
138
+ return Err(format!(
139
+ "opcode space: expected 2^{} = {}, code has {}",
140
+ self.architecture.opcode_width_bits, expected_space, self.opcodes.space
141
+ ));
142
+ }
143
+
144
+ // Assigned opcodes must not exceed space
145
+ if self.opcodes.assigned > self.opcodes.space {
146
+ return Err(format!(
147
+ "assigned opcodes ({}) exceeds opcode space ({})",
148
+ self.opcodes.assigned, self.opcodes.space
149
+ ));
150
+ }
151
+
152
+ // Check register count (spec says 32 GP registers)
153
+ if self.registers.gp_count != 32 {
154
+ return Err(format!(
155
+ "register count: spec says 32, code has {}",
156
+ self.registers.gp_count
157
+ ));
158
+ }
159
+
160
+ // Check that special registers are present
161
+ if self.registers.special_registers.len() != 4 {
162
+ return Err(format!(
163
+ "special registers: expected 4 (PC, SP, FP, FLAGS), code has {}",
164
+ self.registers.special_registers.len()
165
+ ));
166
+ }
167
+
168
+ // Check that all expected flags are present
169
+ if self.flags.flags.len() != 4 {
170
+ return Err(format!(
171
+ "flags: expected 4 (Z, N, C, V), code has {}",
172
+ self.flags.flags.len()
173
+ ));
174
+ }
175
+
176
+ Ok(())
177
+ }
178
+
179
+ /// Serialize to JSON string.
180
+ pub fn to_json(&self) -> serde_json::Result<String> {
181
+ serde_json::to_string_pretty(self)
182
+ }
183
+
184
+ /// Deserialize from JSON string.
185
+ pub fn from_json(json: &str) -> serde_json::Result<Self> {
186
+ serde_json::from_str(json)
187
+ }
188
+ }
189
+
190
+ #[cfg(test)]
191
+ mod tests {
192
+ use super::*;
193
+
194
+ #[test]
195
+ fn schema_generates_from_live_isa() {
196
+ let schema = IsaSchema::generate();
197
+ assert_eq!(schema.architecture.word_width_bits, 64);
198
+ assert_eq!(schema.architecture.address_width_bits, 64);
199
+ assert_eq!(schema.architecture.opcode_width_bits, 8);
200
+ }
201
+
202
+ #[test]
203
+ fn schema_passes_spec_conformance_check() {
204
+ let schema = IsaSchema::generate();
205
+ assert!(schema.check_against_spec().is_ok());
206
+ }
207
+
208
+ #[test]
209
+ fn schema_opcode_space_is_correct() {
210
+ let schema = IsaSchema::generate();
211
+ assert_eq!(schema.opcodes.space, 256);
212
+ assert!(schema.opcodes.assigned <= schema.opcodes.space);
213
+ }
214
+
215
+ #[test]
216
+ fn schema_registers_are_correct() {
217
+ let schema = IsaSchema::generate();
218
+ assert_eq!(schema.registers.gp_count, 32);
219
+ assert_eq!(schema.registers.special_registers.len(), 4);
220
+ }
221
+
222
+ #[test]
223
+ fn schema_flags_are_correct() {
224
+ let schema = IsaSchema::generate();
225
+ assert_eq!(schema.flags.flags.len(), 4);
226
+ }
227
+
228
+ #[test]
229
+ fn conformance_check_catches_word_width_mismatch() {
230
+ let mut schema = IsaSchema::generate();
231
+ schema.architecture.word_width_bits = 32;
232
+ assert!(schema.check_against_spec().is_err());
233
+ }
234
+
235
+ #[test]
236
+ fn conformance_check_catches_address_width_mismatch() {
237
+ let mut schema = IsaSchema::generate();
238
+ schema.architecture.address_width_bits = 32;
239
+ assert!(schema.check_against_spec().is_err());
240
+ }
241
+
242
+ #[test]
243
+ fn conformance_check_catches_opcode_width_mismatch() {
244
+ let mut schema = IsaSchema::generate();
245
+ schema.architecture.opcode_width_bits = 16;
246
+ assert!(schema.check_against_spec().is_err());
247
+ }
248
+
249
+ #[test]
250
+ fn conformance_check_catches_register_count_mismatch() {
251
+ let mut schema = IsaSchema::generate();
252
+ schema.registers.gp_count = 16;
253
+ assert!(schema.check_against_spec().is_err());
254
+ }
255
+
256
+ #[test]
257
+ fn conformance_check_catches_flag_count_mismatch() {
258
+ let mut schema = IsaSchema::generate();
259
+ schema.flags.flags.clear();
260
+ assert!(schema.check_against_spec().is_err());
261
+ }
262
+
263
+ #[test]
264
+ fn schema_serializes_to_json() {
265
+ let schema = IsaSchema::generate();
266
+ let json = schema.to_json().expect("json serialization failed");
267
+ assert!(json.contains("architecture"));
268
+ assert!(json.contains("word_width_bits"));
269
+ }
270
+
271
+ #[test]
272
+ fn schema_deserializes_from_json() {
273
+ let original = IsaSchema::generate();
274
+ let json = original.to_json().expect("serialization failed");
275
+ let deserialized = IsaSchema::from_json(&json).expect("deserialization failed");
276
+ assert_eq!(original, deserialized);
277
+ }
278
+
279
+ #[test]
280
+ fn schema_roundtrip_preserves_structure() {
281
+ let schema = IsaSchema::generate();
282
+ let json = schema.to_json().unwrap();
283
+ let recovered = IsaSchema::from_json(&json).unwrap();
284
+
285
+ assert_eq!(schema.architecture, recovered.architecture);
286
+ assert_eq!(schema.opcodes, recovered.opcodes);
287
+ assert_eq!(schema.registers, recovered.registers);
288
+ assert_eq!(schema.flags, recovered.flags);
289
+ }
290
+
291
+ #[test]
292
+ fn opcode_space_is_power_of_two() {
293
+ let schema = IsaSchema::generate();
294
+ let bits = schema.architecture.opcode_width_bits;
295
+ let expected = 1usize << bits;
296
+ assert_eq!(schema.opcodes.space, expected);
297
+ }
298
+
299
+ #[test]
300
+ fn assigned_opcodes_is_reasonable() {
301
+ let schema = IsaSchema::generate();
302
+ // Should have at least one opcode and less than the space
303
+ assert!(schema.opcodes.assigned > 0);
304
+ assert!(schema.opcodes.assigned < schema.opcodes.space);
305
+ }
306
+ }
crates/jxcl-isa-versioning/Cargo.toml ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ [package]
2
+ name = "jxcl-isa-versioning"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+ description = "ISA/binary-format version negotiation so old binaries fail closed against an incompatible newer decoder rather than silently misdecoding."
6
+ license = "AGPL-3.0-only OR LicenseRef-Commercial"
7
+
8
+ [dependencies]
9
+ jxcl-constants = { path = "../jxcl-constants" }
crates/jxcl-isa-versioning/README.md ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # jxcl-isa-versioning
2
+
3
+ ISA version negotiation and backward-compatibility checking for binaries.
4
+
5
+ ## Purpose
6
+
7
+ Prevents silently misdecoding old binaries when the ISA evolves by enforcing semantic versioning:
8
+ - Major version changes are breaking (not compatible)
9
+ - Minor version changes are backward-compatible (newer decoders can understand old formats)
10
+ - Binary can only run on decoders with same major version and >= minor version
11
+
12
+ ## Public API
13
+
14
+ - `IsaVersion::new(major, minor) -> IsaVersion` - Create a version
15
+ - `IsaVersion::current() -> IsaVersion` - Get the current ISA version
16
+ - `IsaVersion::is_compatible_with(binary_version) -> bool` - Check if decoder can run binary
17
+ - `IsaVersion::is_newer_than_or_equal(other) -> bool` - Version comparison
18
+ - `IsaVersion::is_older_than(other) -> bool` - Version comparison
19
+ - Display via `Display` trait, parsing via `FromStr` trait
20
+
21
+ ## Semantic Versioning Model
22
+
23
+ A decoder supporting version X.Y is compatible with binaries built for X.Z where Z <= Y:
24
+ - Same major version required (breaking changes are not compatible)
25
+ - Decoder's minor >= binary's minor (newer decoders understand old formats)
26
+
27
+ Examples:
28
+ - Decoder 1.2 can run binaries compiled for 1.0, 1.1, 1.2 ✓
29
+ - Decoder 1.0 cannot run binaries compiled for 1.2 ✗
30
+ - Decoder 2.0 cannot run binaries compiled for 1.x ✗
31
+
32
+ ## Testing
33
+
34
+ The crate includes 12 unit tests covering:
35
+ - Current version derivation from binary format version
36
+ - Compatibility checks (same version, older minor, newer minor)
37
+ - Version comparison operations
38
+ - Display formatting and parsing
39
+ - Boundary cases (max version numbers)
40
+ - Ordering relationships
41
+
42
+ ## License
43
+
44
+ AGPLv3 / Commercial dual-license. See `LICENSE-AGPL`, `LICENSE-COMMERCIAL`, and `LICENSE-NOTICE` at the repository root.
crates/jxcl-isa-versioning/src/lib.rs ADDED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Copyright (C) 2026 ERRANT DIGITAL INSTITUTE OF TECHNOLOGY (SEIT NGO) % SNAPKITTY COLLECTIVE
2
+ // SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial
3
+ // See LICENSE-AGPL and LICENSE-COMMERCIAL in this repository for license terms.
4
+
5
+ //! ISA/binary-format version negotiation so old binaries fail closed against an incompatible newer decoder rather than silently misdecoding.
6
+ //!
7
+ //! Owns: `IsaVersion`, its embedding in the binary container header, and
8
+ //! the compatibility check (`is_compatible_with`).
9
+ //!
10
+ //! The ISA version is a semantic versioning scheme where compatibility is
11
+ //! determined by comparing major and minor versions: a binary compiled
12
+ //! against ISA version X.Y can only run on a decoder supporting >= X.Y
13
+ //! (i.e., same major version and minor version >= binary's minor version).
14
+ #![forbid(unsafe_code)]
15
+
16
+ use jxcl_constants::BINARY_VERSION;
17
+
18
+ /// An ISA semantic version (major.minor) embedded in binary containers
19
+ /// to prevent silently misdecoding old binaries when the ISA evolves.
20
+ ///
21
+ /// Following semantic versioning: major changes are breaking, minor
22
+ /// changes are backward-compatible. A decoder supporting version X.Y can
23
+ /// safely decode binaries built for X.0 through X.Y (same major, any
24
+ /// minor <= decoder's minor).
25
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
26
+ pub struct IsaVersion {
27
+ /// Major version number (breaking changes).
28
+ pub major: u16,
29
+ /// Minor version number (backward-compatible additions).
30
+ pub minor: u16,
31
+ }
32
+
33
+ impl IsaVersion {
34
+ /// Create a new ISA version from major and minor components.
35
+ pub const fn new(major: u16, minor: u16) -> Self {
36
+ IsaVersion { major, minor }
37
+ }
38
+
39
+ /// The current ISA version embedded in all newly created binaries.
40
+ /// Sourced from `jxcl-constants::BINARY_VERSION` which is the
41
+ /// single authoritative version number.
42
+ pub const fn current() -> Self {
43
+ // BINARY_VERSION is a u16; we treat it as major=1, minor=VERSION
44
+ // for forward compatibility (allows future major versions).
45
+ IsaVersion {
46
+ major: 1,
47
+ minor: BINARY_VERSION,
48
+ }
49
+ }
50
+
51
+ /// Check if this decoder's ISA version is compatible with a binary
52
+ /// compiled for `binary_version`.
53
+ ///
54
+ /// A decoder supporting version X.Y is compatible with binaries
55
+ /// built for X.Z where Z <= Y (same major, any older minor).
56
+ /// This is true iff:
57
+ /// - Major versions match (breaking changes are not forward-compatible)
58
+ /// - Decoder's minor >= binary's minor (newer decoders understand old formats)
59
+ pub const fn is_compatible_with(self, binary_version: IsaVersion) -> bool {
60
+ self.major == binary_version.major && self.minor >= binary_version.minor
61
+ }
62
+
63
+ /// Check if this is a newer or equal version than another.
64
+ pub const fn is_newer_than_or_equal(self, other: IsaVersion) -> bool {
65
+ self.major > other.major || (self.major == other.major && self.minor >= other.minor)
66
+ }
67
+
68
+ /// Check if this is older than another version.
69
+ pub const fn is_older_than(self, other: IsaVersion) -> bool {
70
+ self.major < other.major || (self.major == other.major && self.minor < other.minor)
71
+ }
72
+ }
73
+
74
+ impl std::fmt::Display for IsaVersion {
75
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76
+ write!(f, "{}.{}", self.major, self.minor)
77
+ }
78
+ }
79
+
80
+ impl std::str::FromStr for IsaVersion {
81
+ type Err = String;
82
+
83
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
84
+ let parts: Vec<&str> = s.split('.').collect();
85
+ if parts.len() != 2 {
86
+ return Err(format!(
87
+ "invalid version format: expected 'major.minor', got '{}'",
88
+ s
89
+ ));
90
+ }
91
+
92
+ let major = parts[0]
93
+ .parse::<u16>()
94
+ .map_err(|_| format!("invalid major version: {}", parts[0]))?;
95
+ let minor = parts[1]
96
+ .parse::<u16>()
97
+ .map_err(|_| format!("invalid minor version: {}", parts[1]))?;
98
+
99
+ Ok(IsaVersion { major, minor })
100
+ }
101
+ }
102
+
103
+ #[cfg(test)]
104
+ mod tests {
105
+ use super::*;
106
+
107
+ #[test]
108
+ fn current_version_is_well_formed() {
109
+ let current = IsaVersion::current();
110
+ assert_eq!(current.major, 1);
111
+ assert_eq!(current.minor, BINARY_VERSION);
112
+ }
113
+
114
+ #[test]
115
+ fn compatible_with_same_version() {
116
+ let v1_0 = IsaVersion::new(1, 0);
117
+ assert!(v1_0.is_compatible_with(v1_0));
118
+ }
119
+
120
+ #[test]
121
+ fn compatible_with_older_minor() {
122
+ let decoder = IsaVersion::new(1, 2);
123
+ let binary = IsaVersion::new(1, 0);
124
+ assert!(decoder.is_compatible_with(binary));
125
+ }
126
+
127
+ #[test]
128
+ fn compatible_with_equal_minor() {
129
+ let decoder = IsaVersion::new(1, 2);
130
+ let binary = IsaVersion::new(1, 2);
131
+ assert!(decoder.is_compatible_with(binary));
132
+ }
133
+
134
+ #[test]
135
+ fn not_compatible_with_newer_minor() {
136
+ let decoder = IsaVersion::new(1, 0);
137
+ let binary = IsaVersion::new(1, 2);
138
+ assert!(!decoder.is_compatible_with(binary));
139
+ }
140
+
141
+ #[test]
142
+ fn not_compatible_with_different_major() {
143
+ let decoder = IsaVersion::new(1, 5);
144
+ let binary = IsaVersion::new(2, 0);
145
+ assert!(!decoder.is_compatible_with(binary));
146
+ }
147
+
148
+ #[test]
149
+ fn version_comparison_operations() {
150
+ let v1_0 = IsaVersion::new(1, 0);
151
+ let v1_2 = IsaVersion::new(1, 2);
152
+ let v2_0 = IsaVersion::new(2, 0);
153
+
154
+ assert!(v1_2.is_newer_than_or_equal(v1_0));
155
+ assert!(v1_2.is_newer_than_or_equal(v1_2));
156
+ assert!(!v1_0.is_newer_than_or_equal(v1_2));
157
+
158
+ assert!(v1_0.is_older_than(v1_2));
159
+ assert!(!v1_2.is_older_than(v1_0));
160
+ assert!(v1_2.is_older_than(v2_0));
161
+ }
162
+
163
+ #[test]
164
+ fn version_display_format() {
165
+ let v = IsaVersion::new(1, 5);
166
+ assert_eq!(v.to_string(), "1.5");
167
+ }
168
+
169
+ #[test]
170
+ fn version_parsing() {
171
+ let v: IsaVersion = "1.5".parse().unwrap();
172
+ assert_eq!(v.major, 1);
173
+ assert_eq!(v.minor, 5);
174
+ }
175
+
176
+ #[test]
177
+ fn version_parsing_errors() {
178
+ assert!("1".parse::<IsaVersion>().is_err());
179
+ assert!("1.2.3".parse::<IsaVersion>().is_err());
180
+ assert!("a.b".parse::<IsaVersion>().is_err());
181
+ }
182
+
183
+ #[test]
184
+ fn version_ordering() {
185
+ let v1_0 = IsaVersion::new(1, 0);
186
+ let v1_2 = IsaVersion::new(1, 2);
187
+ let v2_0 = IsaVersion::new(2, 0);
188
+
189
+ assert!(v1_0 < v1_2);
190
+ assert!(v1_2 < v2_0);
191
+ assert!(v1_0 < v2_0);
192
+ }
193
+
194
+ #[test]
195
+ fn boundary_max_version_numbers() {
196
+ let v_max = IsaVersion::new(u16::MAX, u16::MAX);
197
+ assert_eq!(v_max.major, u16::MAX);
198
+ assert_eq!(v_max.minor, u16::MAX);
199
+ assert!(v_max.is_compatible_with(v_max));
200
+ }
201
+ }
crates/jxcl-lexer/Cargo.toml ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ [package]
2
+ name = "jxcl-lexer"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+ description = "Tokenizes assembly source text."
6
+ license = "AGPL-3.0-only OR LicenseRef-Commercial"
7
+
8
+ [dependencies]
9
+ jxcl-errors = { path = "../jxcl-errors" }
crates/jxcl-lexer/README.md ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # jxcl-lexer
2
+
3
+ Tokenizes assembly source text.
4
+
5
+ ## Architecture
6
+
7
+ **Owns:** The Token type and lexer.
8
+
9
+ **Category:** toolchain · **Source:** extraction:jxcl/src/assembler/lexer.rs
10
+
11
+ ## Public API
12
+
13
+ `lex`, `Token`
14
+
15
+ ## Dependencies
16
+
17
+ Workspace crates:
18
+
19
+ - `jxcl-errors`
20
+
21
+ External crates:
22
+
23
+ *(none)*
24
+
25
+ ## Testing
26
+
27
+ Planned test kinds: unit, boundary.
28
+
29
+ ## Status
30
+
31
+ `planned` in `docs/crates.toml` -- see
32
+ `docs/CRATE_GENERATION_PLAN.md` for the implementation batch schedule.
33
+
34
+ ## License
35
+
36
+ AGPLv3 / Commercial dual-license. See `LICENSE-AGPL`, `LICENSE-COMMERCIAL`, and `LICENSE-NOTICE` at the repository root.
crates/jxcl-lexer/src/lib.rs ADDED
@@ -0,0 +1,271 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Copyright (C) 2026 ERRANT DIGITAL INSTITUTE OF TECHNOLOGY (SEIT NGO) % SNAPKITTY COLLECTIVE
2
+ // SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial
3
+ // See LICENSE-AGPL and LICENSE-COMMERCIAL in this repository for license terms.
4
+
5
+ //! Tokenizes assembly source text.
6
+ //!
7
+ //! Owns: the [`Token`] type and [`lex`]/[`lex_line`] -- extracted, near
8
+ //! verbatim, from `jxcl`'s original `src/assembler/lexer.rs`. JXCL
9
+ //! assembly is line-oriented -- every statement (label, directive, or
10
+ //! instruction) occupies exactly one line -- which keeps the grammar
11
+ //! simple and error locations exact.
12
+ //!
13
+ //! `docs/crates.toml` lists `jxcl-errors` as this crate's one dependency
14
+ //! (for the crate-wide error type). At the time this crate was
15
+ //! implemented, `jxcl-errors` was still a scaffolded placeholder with no
16
+ //! public items, so [`LexError`] is a small local error type scoped to
17
+ //! lexing, matching the shape (`kind`/`line`/`reason`) of what
18
+ //! `jxcl-errors`'s `AssemblerErrorKind` variants would produce. Once
19
+ //! `jxcl-errors` lands, this can become a thin wrapper (or direct
20
+ //! re-export) instead of an independent type.
21
+
22
+ #![forbid(unsafe_code)]
23
+
24
+ use std::fmt;
25
+
26
+ #[derive(Debug, Clone, PartialEq, Eq)]
27
+ pub enum Token {
28
+ /// A bare word: mnemonic, register name, label, or constant reference.
29
+ Ident(String),
30
+ /// A directive keyword, without its leading `.` (e.g. `.data` -> `"data"`).
31
+ Directive(String),
32
+ /// A numeric literal (decimal, `0x` hex, or `0b` binary), already parsed.
33
+ Number(i128),
34
+ Comma,
35
+ Colon,
36
+ LBracket,
37
+ RBracket,
38
+ Plus,
39
+ Minus,
40
+ }
41
+
42
+ #[derive(Debug, Clone, PartialEq, Eq)]
43
+ pub enum LexErrorKind {
44
+ MalformedImmediate,
45
+ UnexpectedToken,
46
+ }
47
+
48
+ #[derive(Debug, Clone, PartialEq, Eq)]
49
+ pub struct LexError {
50
+ pub kind: LexErrorKind,
51
+ pub line: usize,
52
+ pub reason: String,
53
+ }
54
+
55
+ impl fmt::Display for LexError {
56
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57
+ write!(
58
+ f,
59
+ "lex error at line {}: {:?}: {}",
60
+ self.line, self.kind, self.reason
61
+ )
62
+ }
63
+ }
64
+ impl std::error::Error for LexError {}
65
+
66
+ fn is_ident_start(c: char) -> bool {
67
+ c.is_ascii_alphabetic() || c == '_'
68
+ }
69
+ fn is_ident_continue(c: char) -> bool {
70
+ c.is_ascii_alphanumeric() || c == '_'
71
+ }
72
+
73
+ fn parse_number(text: &str, line: usize) -> Result<i128, LexError> {
74
+ let bad = |reason: String| LexError {
75
+ kind: LexErrorKind::MalformedImmediate,
76
+ line,
77
+ reason,
78
+ };
79
+ if let Some(hex) = text.strip_prefix("0x").or_else(|| text.strip_prefix("0X")) {
80
+ i128::from_str_radix(hex, 16)
81
+ .map_err(|e| bad(format!("invalid hex literal {:?}: {}", text, e)))
82
+ } else if let Some(bin) = text.strip_prefix("0b").or_else(|| text.strip_prefix("0B")) {
83
+ i128::from_str_radix(bin, 2)
84
+ .map_err(|e| bad(format!("invalid binary literal {:?}: {}", text, e)))
85
+ } else {
86
+ text.parse::<i128>()
87
+ .map_err(|e| bad(format!("invalid decimal literal {:?}: {}", text, e)))
88
+ }
89
+ }
90
+
91
+ /// Tokenize one line of JXCL assembly source. `;` begins a line comment
92
+ /// that runs to end of line.
93
+ pub fn lex_line(line_text: &str, line: usize) -> Result<Vec<Token>, LexError> {
94
+ let mut tokens = Vec::new();
95
+ let chars: Vec<char> = line_text.chars().collect();
96
+ let mut i = 0usize;
97
+
98
+ while i < chars.len() {
99
+ let c = chars[i];
100
+ match c {
101
+ ' ' | '\t' | '\r' => {
102
+ i += 1;
103
+ }
104
+ ';' => break, // rest of line is a comment
105
+ ',' => {
106
+ tokens.push(Token::Comma);
107
+ i += 1;
108
+ }
109
+ ':' => {
110
+ tokens.push(Token::Colon);
111
+ i += 1;
112
+ }
113
+ '[' => {
114
+ tokens.push(Token::LBracket);
115
+ i += 1;
116
+ }
117
+ ']' => {
118
+ tokens.push(Token::RBracket);
119
+ i += 1;
120
+ }
121
+ '+' => {
122
+ tokens.push(Token::Plus);
123
+ i += 1;
124
+ }
125
+ '-' => {
126
+ tokens.push(Token::Minus);
127
+ i += 1;
128
+ }
129
+ '.' => {
130
+ let start = i + 1;
131
+ let mut j = start;
132
+ while j < chars.len() && is_ident_continue(chars[j]) {
133
+ j += 1;
134
+ }
135
+ if j == start {
136
+ return Err(LexError {
137
+ kind: LexErrorKind::UnexpectedToken,
138
+ line,
139
+ reason: "'.' not followed by a directive name".into(),
140
+ });
141
+ }
142
+ tokens.push(Token::Directive(chars[start..j].iter().collect()));
143
+ i = j;
144
+ }
145
+ c if c.is_ascii_digit() => {
146
+ let start = i;
147
+ let mut j = i + 1;
148
+ while j < chars.len() && is_ident_continue(chars[j]) {
149
+ j += 1;
150
+ }
151
+ let text: String = chars[start..j].iter().collect();
152
+ tokens.push(Token::Number(parse_number(&text, line)?));
153
+ i = j;
154
+ }
155
+ c if is_ident_start(c) => {
156
+ let start = i;
157
+ let mut j = i + 1;
158
+ while j < chars.len() && is_ident_continue(chars[j]) {
159
+ j += 1;
160
+ }
161
+ tokens.push(Token::Ident(chars[start..j].iter().collect()));
162
+ i = j;
163
+ }
164
+ other => {
165
+ return Err(LexError {
166
+ kind: LexErrorKind::UnexpectedToken,
167
+ line,
168
+ reason: format!("unexpected character {:?}", other),
169
+ });
170
+ }
171
+ }
172
+ }
173
+
174
+ Ok(tokens)
175
+ }
176
+
177
+ /// Tokenize a complete multi-line source file, one line at a time.
178
+ /// Returns one entry per source line, in order, as `(1-based line number,
179
+ /// that line's tokens)`. A blank or comment-only line yields an empty
180
+ /// token vector rather than being skipped, so line numbers in the result
181
+ /// always match the source file exactly.
182
+ pub fn lex(source: &str) -> Result<Vec<(usize, Vec<Token>)>, LexError> {
183
+ let mut out = Vec::new();
184
+ for (idx, raw_line) in source.lines().enumerate() {
185
+ let line_no = idx + 1;
186
+ out.push((line_no, lex_line(raw_line, line_no)?));
187
+ }
188
+ Ok(out)
189
+ }
190
+
191
+ #[cfg(test)]
192
+ mod tests {
193
+ use super::*;
194
+
195
+ #[test]
196
+ fn tokenizes_instruction_line() {
197
+ let toks = lex_line(" MOVI R1, 10", 1).unwrap();
198
+ assert_eq!(
199
+ toks,
200
+ vec![
201
+ Token::Ident("MOVI".into()),
202
+ Token::Ident("R1".into()),
203
+ Token::Comma,
204
+ Token::Number(10),
205
+ ]
206
+ );
207
+ }
208
+
209
+ #[test]
210
+ fn tokenizes_label() {
211
+ let toks = lex_line("start:", 1).unwrap();
212
+ assert_eq!(toks, vec![Token::Ident("start".into()), Token::Colon]);
213
+ }
214
+
215
+ #[test]
216
+ fn tokenizes_hex_and_binary() {
217
+ let toks = lex_line("0x1A 0b1010", 1).unwrap();
218
+ assert_eq!(toks, vec![Token::Number(0x1A), Token::Number(0b1010)]);
219
+ }
220
+
221
+ #[test]
222
+ fn strips_comments() {
223
+ let toks = lex_line("HALT ; stop here", 1).unwrap();
224
+ assert_eq!(toks, vec![Token::Ident("HALT".into())]);
225
+ }
226
+
227
+ #[test]
228
+ fn tokenizes_memory_operand() {
229
+ let toks = lex_line("[R2+8]", 1).unwrap();
230
+ assert_eq!(
231
+ toks,
232
+ vec![
233
+ Token::LBracket,
234
+ Token::Ident("R2".into()),
235
+ Token::Plus,
236
+ Token::Number(8),
237
+ Token::RBracket
238
+ ]
239
+ );
240
+ }
241
+
242
+ #[test]
243
+ fn tokenizes_directive() {
244
+ let toks = lex_line(".data", 1).unwrap();
245
+ assert_eq!(toks, vec![Token::Directive("data".into())]);
246
+ }
247
+
248
+ #[test]
249
+ fn lex_whole_source_preserves_line_numbers() {
250
+ let src = "MOVI R1, 1\n\nHALT\n";
251
+ let lines = lex(src).unwrap();
252
+ assert_eq!(lines.len(), 3);
253
+ assert_eq!(lines[0].0, 1);
254
+ assert_eq!(lines[1].0, 2);
255
+ assert!(lines[1].1.is_empty());
256
+ assert_eq!(lines[2].0, 3);
257
+ assert_eq!(lines[2].1, vec![Token::Ident("HALT".into())]);
258
+ }
259
+
260
+ #[test]
261
+ fn rejects_unexpected_character() {
262
+ let err = lex_line("MOVI R1, @", 1).unwrap_err();
263
+ assert_eq!(err.kind, LexErrorKind::UnexpectedToken);
264
+ }
265
+
266
+ #[test]
267
+ fn rejects_malformed_number() {
268
+ let err = lex_line("0xZZ", 1).unwrap_err();
269
+ assert_eq!(err.kind, LexErrorKind::MalformedImmediate);
270
+ }
271
+ }
crates/jxcl-linker/Cargo.toml ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [package]
2
+ name = "jxcl-linker"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+ description = "Resolves symbols and applies relocations across one or more object files into a single linked binary container."
6
+ license = "AGPL-3.0-only OR LicenseRef-Commercial"
7
+
8
+ [dependencies]
9
+ jxcl-object = { path = "../jxcl-object" }
10
+ jxcl-symbols = { path = "../jxcl-symbols" }
11
+ jxcl-relocations = { path = "../jxcl-relocations" }
12
+ jxcl-binary = { path = "../jxcl-binary" }
13
+ jxcl-errors = { path = "../jxcl-errors" }
crates/jxcl-linker/README.md ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # jxcl-linker
2
+
3
+ Resolves symbols and applies relocations across one or more object files into a single linked binary container.
4
+
5
+ ## Architecture
6
+
7
+ **Owns:** link(&[ObjectFile]) -> BinaryContainer.
8
+
9
+ **Category:** toolchain · **Source:** new
10
+
11
+ ## Public API
12
+
13
+ `link`
14
+
15
+ ## Dependencies
16
+
17
+ Workspace crates:
18
+
19
+ - `jxcl-object`
20
+ - `jxcl-symbols`
21
+ - `jxcl-relocations`
22
+ - `jxcl-binary`
23
+ - `jxcl-errors`
24
+
25
+ External crates:
26
+
27
+ *(none)*
28
+
29
+ ## Testing
30
+
31
+ Planned test kinds: unit, integration, error-path.
32
+
33
+ ## Status
34
+
35
+ `planned` in `docs/crates.toml` -- see
36
+ `docs/CRATE_GENERATION_PLAN.md` for the implementation batch schedule.
37
+
38
+ ## License
39
+
40
+ AGPLv3 / Commercial dual-license. See `LICENSE-AGPL`, `LICENSE-COMMERCIAL`, and `LICENSE-NOTICE` at the repository root.
crates/jxcl-linker/src/lib.rs ADDED
@@ -0,0 +1,335 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Copyright (C) 2026 ERRANT DIGITAL INSTITUTE OF TECHNOLOGY (SEIT NGO) % SNAPKITTY COLLECTIVE
2
+ // SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial
3
+ // See LICENSE-AGPL and LICENSE-COMMERCIAL in this repository for license terms.
4
+
5
+ //! Resolves symbols and applies relocations across one or more object
6
+ //! files into a single linked binary container.
7
+ //!
8
+ //! New functionality (see `jxcl-object`'s module docs for why there is
9
+ //! no pre-expansion ground truth to extract this from -- the original
10
+ //! `jxcl` assembler always went straight from source to a finished
11
+ //! binary in one step, with no separate-compilation/linking phase at
12
+ //! all).
13
+ //!
14
+ //! ## Layout
15
+ //!
16
+ //! [`link`] concatenates every object file's code section, in the order
17
+ //! given, into one combined code section, and likewise for data
18
+ //! sections. Matching `jxcl-binary`'s convention, the combined data
19
+ //! section is placed immediately after the combined code section, so a
20
+ //! `Data`-section symbol's final absolute address is
21
+ //! `total_code_size + <that object's data section's start within the
22
+ //! combined data section> + <the symbol's original section-local
23
+ //! address>`.
24
+ //!
25
+ //! Every object's [`jxcl_symbols::SymbolTable`] entries are re-based to
26
+ //! these final absolute addresses and merged into one combined table; a
27
+ //! name defined in more than one input object is a
28
+ //! [`LinkErrorKind::DuplicateSymbol`] error (linking never silently
29
+ //! picks one definition over another). Every relocation is then applied,
30
+ //! via [`jxcl_relocations::apply_relocation`], against the combined
31
+ //! buffer at its correctly re-based absolute offset; a relocation whose
32
+ //! symbol resolves in no object is a [`LinkErrorKind::UndefinedSymbol`]
33
+ //! error.
34
+ //!
35
+ //! At most one input object may set
36
+ //! [`jxcl_object::ObjectFile::entry_symbol`]; if none do, the linked
37
+ //! program's entry point defaults to `0` (matching the assembler's own
38
+ //! default).
39
+ #![forbid(unsafe_code)]
40
+
41
+ use std::fmt;
42
+
43
+ use jxcl_binary::BinaryContainer;
44
+ use jxcl_object::{ObjectFile, RelocationTarget};
45
+ use jxcl_relocations::{apply_relocation, Relocation};
46
+ use jxcl_symbols::SymbolTable;
47
+
48
+ #[derive(Debug, Clone, PartialEq, Eq)]
49
+ pub enum LinkErrorKind {
50
+ /// The same symbol name was defined by more than one input object.
51
+ DuplicateSymbol,
52
+ /// A relocation (or the entry point) referenced a symbol no input
53
+ /// object defines.
54
+ UndefinedSymbol,
55
+ /// More than one input object set `entry_symbol`.
56
+ MultipleEntryPoints,
57
+ /// A relocation's field didn't fit the resolved address (e.g. a
58
+ /// `PcRelative32` displacement that overflowed 32 bits).
59
+ RelocationFailed,
60
+ }
61
+
62
+ #[derive(Debug, Clone, PartialEq, Eq)]
63
+ pub struct LinkError {
64
+ pub kind: LinkErrorKind,
65
+ pub reason: String,
66
+ }
67
+
68
+ impl fmt::Display for LinkError {
69
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70
+ write!(f, "link error: {:?}: {}", self.kind, self.reason)
71
+ }
72
+ }
73
+ impl std::error::Error for LinkError {}
74
+
75
+ fn err(kind: LinkErrorKind, reason: impl Into<String>) -> LinkError {
76
+ LinkError {
77
+ kind,
78
+ reason: reason.into(),
79
+ }
80
+ }
81
+
82
+ /// Link one or more relocatable object files into a single runnable
83
+ /// [`BinaryContainer`].
84
+ pub fn link(objects: &[ObjectFile]) -> Result<BinaryContainer, LinkError> {
85
+ // Pass 1: compute each object's placement (running code/data offsets)
86
+ // and the combined section sizes.
87
+ let mut code_offsets = Vec::with_capacity(objects.len());
88
+ let mut data_offsets = Vec::with_capacity(objects.len());
89
+ let mut code_cursor: u64 = 0;
90
+ let mut data_cursor: u64 = 0;
91
+ for obj in objects {
92
+ code_offsets.push(code_cursor);
93
+ data_offsets.push(data_cursor);
94
+ code_cursor += obj.code.len() as u64;
95
+ data_cursor += obj.data.len() as u64;
96
+ }
97
+ let total_code_size = code_cursor;
98
+
99
+ // Pass 2: build the combined symbol table at final absolute addresses.
100
+ let mut combined_symbols = SymbolTable::new();
101
+ for (i, obj) in objects.iter().enumerate() {
102
+ for sym in obj.symbols.iter() {
103
+ let abs_address = match sym.section {
104
+ jxcl_symbols::SymbolSection::Code => code_offsets[i] + sym.address,
105
+ jxcl_symbols::SymbolSection::Data => {
106
+ total_code_size + data_offsets[i] + sym.address
107
+ }
108
+ };
109
+ combined_symbols
110
+ .define(sym.name.clone(), abs_address, sym.section)
111
+ .map_err(|_| {
112
+ err(
113
+ LinkErrorKind::DuplicateSymbol,
114
+ format!(
115
+ "symbol {:?} is defined in more than one object file",
116
+ sym.name
117
+ ),
118
+ )
119
+ })?;
120
+ }
121
+ }
122
+
123
+ // Pass 3: concatenate code and data into one combined buffer (code
124
+ // first, data immediately after -- matching jxcl-binary's layout).
125
+ let mut combined: Vec<u8> = Vec::with_capacity(
126
+ objects.iter().map(|o| o.code.len()).sum::<usize>()
127
+ + objects.iter().map(|o| o.data.len()).sum::<usize>(),
128
+ );
129
+ for obj in objects {
130
+ combined.extend_from_slice(&obj.code);
131
+ }
132
+ for obj in objects {
133
+ combined.extend_from_slice(&obj.data);
134
+ }
135
+
136
+ // Pass 4: apply every relocation at its re-based absolute offset.
137
+ for (i, obj) in objects.iter().enumerate() {
138
+ for tr in &obj.relocations {
139
+ let abs_offset = match tr.target {
140
+ RelocationTarget::Code => code_offsets[i] + tr.relocation.offset,
141
+ RelocationTarget::Data => total_code_size + data_offsets[i] + tr.relocation.offset,
142
+ };
143
+ let resolved = combined_symbols
144
+ .resolve(&tr.relocation.symbol)
145
+ .ok_or_else(|| {
146
+ err(
147
+ LinkErrorKind::UndefinedSymbol,
148
+ format!("undefined symbol {:?}", tr.relocation.symbol),
149
+ )
150
+ })?;
151
+ let reloc =
152
+ Relocation::new(abs_offset, tr.relocation.kind, tr.relocation.symbol.clone());
153
+ apply_relocation(&mut combined, &reloc, resolved.address)
154
+ .map_err(|e| err(LinkErrorKind::RelocationFailed, e.to_string()))?;
155
+ }
156
+ }
157
+
158
+ // Pass 5: resolve the entry point, if any object nominated one.
159
+ let mut entry_symbol: Option<&str> = None;
160
+ for obj in objects {
161
+ if let Some(name) = &obj.entry_symbol {
162
+ if entry_symbol.is_some() {
163
+ return Err(err(
164
+ LinkErrorKind::MultipleEntryPoints,
165
+ "more than one object file set an entry symbol",
166
+ ));
167
+ }
168
+ entry_symbol = Some(name.as_str());
169
+ }
170
+ }
171
+ let entry_point = match entry_symbol {
172
+ Some(name) => {
173
+ combined_symbols
174
+ .resolve(name)
175
+ .ok_or_else(|| {
176
+ err(
177
+ LinkErrorKind::UndefinedSymbol,
178
+ format!("entry symbol {:?} is undefined", name),
179
+ )
180
+ })?
181
+ .address
182
+ }
183
+ None => 0,
184
+ };
185
+
186
+ let (code, data) = combined.split_at(total_code_size as usize);
187
+ Ok(BinaryContainer::new(
188
+ entry_point,
189
+ code.to_vec(),
190
+ data.to_vec(),
191
+ ))
192
+ }
193
+
194
+ #[cfg(test)]
195
+ mod tests {
196
+ use super::*;
197
+ use jxcl_object::TargetedRelocation;
198
+ use jxcl_relocations::RelocationKind;
199
+ use jxcl_symbols::SymbolSection;
200
+
201
+ #[test]
202
+ fn links_a_single_object_with_no_relocations() {
203
+ let obj = ObjectFile::new(vec![0x60], vec![]).with_entry_symbol("start");
204
+ let mut obj = obj;
205
+ obj.symbols.define("start", 0, SymbolSection::Code).unwrap();
206
+ let bin = link(&[obj]).unwrap();
207
+ assert_eq!(bin.entry_point, 0);
208
+ assert_eq!(bin.code, vec![0x60]);
209
+ }
210
+
211
+ #[test]
212
+ fn defaults_entry_point_to_zero_when_unset() {
213
+ let obj = ObjectFile::new(vec![0x60], vec![]);
214
+ let bin = link(&[obj]).unwrap();
215
+ assert_eq!(bin.entry_point, 0);
216
+ }
217
+
218
+ #[test]
219
+ fn links_two_objects_with_a_cross_reference() {
220
+ // Object A: a 5-byte "CALL helper" placeholder instruction
221
+ // (1 opcode byte + 4-byte PC-relative displacement) followed by
222
+ // a 1-byte HALT. It references "helper", defined in object B.
223
+ let mut a = ObjectFile::new(vec![0xABu8, 0, 0, 0, 0, 0x60], vec![]);
224
+ a.relocations.push(TargetedRelocation::new(
225
+ jxcl_object::RelocationTarget::Code,
226
+ jxcl_relocations::Relocation::new(1, RelocationKind::PcRelative32, "helper"),
227
+ ));
228
+ a = a.with_entry_symbol("main");
229
+ a.symbols.define("main", 0, SymbolSection::Code).unwrap();
230
+
231
+ // Object B: "helper" is a single HALT at its own offset 0.
232
+ let mut b = ObjectFile::new(vec![0x60], vec![]);
233
+ b.symbols.define("helper", 0, SymbolSection::Code).unwrap();
234
+
235
+ let bin = link(&[a, b]).unwrap();
236
+ // Object A occupies bytes [0, 6); object B is appended at 6.
237
+ assert_eq!(bin.code.len(), 7);
238
+ assert_eq!(bin.entry_point, 0);
239
+ // The relocation field is bytes [1, 5); pc_after_fetch = 5;
240
+ // helper's final absolute address is 6 (start of object B's code).
241
+ let disp = i32::from_le_bytes(bin.code[1..5].try_into().unwrap());
242
+ assert_eq!(disp, 1); // 6 - 5
243
+ assert_eq!(bin.code[6], 0x60);
244
+ }
245
+
246
+ #[test]
247
+ fn cross_referencing_a_data_symbol_resolves_past_the_combined_code_section() {
248
+ // A single object whose code references a data-section symbol
249
+ // via an absolute-64 relocation (like MOVI loading a label's
250
+ // address). Exercises the code-size-relative data placement.
251
+ let mut obj = ObjectFile::new(
252
+ vec![0x02u8, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0x60], // MOVI-shaped + HALT
253
+ vec![0xEFu8, 0xBE, 0, 0, 0, 0, 0, 0],
254
+ );
255
+ obj.relocations.push(TargetedRelocation::new(
256
+ jxcl_object::RelocationTarget::Code,
257
+ jxcl_relocations::Relocation::new(2, RelocationKind::Absolute64, "buffer"),
258
+ ));
259
+ obj.symbols
260
+ .define("buffer", 0, SymbolSection::Data)
261
+ .unwrap();
262
+
263
+ let bin = link(&[obj]).unwrap();
264
+ assert_eq!(bin.code.len(), 11);
265
+ assert_eq!(bin.data.len(), 8);
266
+ // "buffer"'s absolute address equals the total code size (11).
267
+ let patched = u64::from_le_bytes(bin.code[2..10].try_into().unwrap());
268
+ assert_eq!(patched, 11);
269
+ }
270
+
271
+ #[test]
272
+ fn undefined_symbol_relocation_is_an_error() {
273
+ let mut obj = ObjectFile::new(vec![0xABu8, 0, 0, 0, 0], vec![]);
274
+ obj.relocations.push(TargetedRelocation::new(
275
+ jxcl_object::RelocationTarget::Code,
276
+ jxcl_relocations::Relocation::new(1, RelocationKind::PcRelative32, "nowhere"),
277
+ ));
278
+ let err = link(&[obj]).unwrap_err();
279
+ assert_eq!(err.kind, LinkErrorKind::UndefinedSymbol);
280
+ }
281
+
282
+ #[test]
283
+ fn duplicate_symbol_across_objects_is_an_error() {
284
+ let mut a = ObjectFile::new(vec![0x60], vec![]);
285
+ a.symbols.define("dup", 0, SymbolSection::Code).unwrap();
286
+ let mut b = ObjectFile::new(vec![0x60], vec![]);
287
+ b.symbols.define("dup", 0, SymbolSection::Code).unwrap();
288
+
289
+ let err = link(&[a, b]).unwrap_err();
290
+ assert_eq!(err.kind, LinkErrorKind::DuplicateSymbol);
291
+ }
292
+
293
+ #[test]
294
+ fn multiple_entry_points_is_an_error() {
295
+ let mut a = ObjectFile::new(vec![0x60], vec![]).with_entry_symbol("a_main");
296
+ a.symbols.define("a_main", 0, SymbolSection::Code).unwrap();
297
+ let mut b = ObjectFile::new(vec![0x60], vec![]).with_entry_symbol("b_main");
298
+ b.symbols.define("b_main", 0, SymbolSection::Code).unwrap();
299
+
300
+ let err = link(&[a, b]).unwrap_err();
301
+ assert_eq!(err.kind, LinkErrorKind::MultipleEntryPoints);
302
+ }
303
+
304
+ #[test]
305
+ fn undefined_entry_symbol_is_an_error() {
306
+ let obj = ObjectFile::new(vec![0x60], vec![]).with_entry_symbol("ghost");
307
+ let err = link(&[obj]).unwrap_err();
308
+ assert_eq!(err.kind, LinkErrorKind::UndefinedSymbol);
309
+ }
310
+
311
+ #[test]
312
+ fn relocation_displacement_out_of_range_is_an_error() {
313
+ let mut obj = ObjectFile::new(vec![0xABu8, 0, 0, 0, 0], vec![]);
314
+ obj.relocations.push(TargetedRelocation::new(
315
+ jxcl_object::RelocationTarget::Code,
316
+ jxcl_relocations::Relocation::new(1, RelocationKind::PcRelative32, "far"),
317
+ ));
318
+ // A symbol whose address is nowhere near the relocation site --
319
+ // still representable as a u64, but its computed displacement
320
+ // overflows i32.
321
+ obj.symbols
322
+ .define("far", u64::MAX / 2, SymbolSection::Code)
323
+ .unwrap();
324
+ let err = link(&[obj]).unwrap_err();
325
+ assert_eq!(err.kind, LinkErrorKind::RelocationFailed);
326
+ }
327
+
328
+ #[test]
329
+ fn empty_object_list_links_to_an_empty_binary() {
330
+ let bin = link(&[]).unwrap();
331
+ assert!(bin.code.is_empty());
332
+ assert!(bin.data.is_empty());
333
+ assert_eq!(bin.entry_point, 0);
334
+ }
335
+ }
crates/jxcl-load-store/Cargo.toml ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [package]
2
+ name = "jxcl-load-store"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+ description = "Typed, sign-extension-aware load/store helpers (u8/u16/u32/u64 and signed variants) between raw memory and the execution engine."
6
+ license = "AGPL-3.0-only OR LicenseRef-Commercial"
7
+
8
+ [dependencies]
9
+ jxcl-memory = { path = "../jxcl-memory" }
10
+ jxcl-endian = { path = "../jxcl-endian" }
11
+ jxcl-exceptions = { path = "../jxcl-exceptions" }
12
+ jxcl-types = { path = "../jxcl-types" }
13
+
14
+ [dev-dependencies]
15
+ jxcl-errors = { path = "../jxcl-errors" }
crates/jxcl-load-store/README.md ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # jxcl-load-store
2
+
3
+ Typed, sign-extension-aware load/store helpers (u8/u16/u32/u64 and signed variants) between raw memory and the execution engine.
4
+
5
+ ## Architecture
6
+
7
+ **Owns:** `load_u8/16/32/64`, `store_u8/16/32/64` and their signed
8
+ counterparts (`load_i8/16/32/64`, `store_i8/16/32/64`).
9
+
10
+ **Category:** memory · **Source:** new (generalizes
11
+ `crates/jxcl/src/execution.rs`'s direct `state.memory.read64`/`write64`
12
+ + `try_mem!` fault-conversion pattern to every width and to signed
13
+ values, converting into `jxcl-exceptions::Exception`).
14
+
15
+ ## Public API
16
+
17
+ `load_u8`, `load_u16`, `load_u32`, `load_u64`, `load_i8`, `load_i16`,
18
+ `load_i32`, `load_i64`, `store_u8`, `store_u16`, `store_u32`,
19
+ `store_u64`, `store_i8`, `store_i16`, `store_i32`, `store_i64` -- all
20
+ operating on a `jxcl-memory::Memory` at a `jxcl-types::Address`,
21
+ returning `Result<_, jxcl_exceptions::Exception>`.
22
+
23
+ ## Dependencies
24
+
25
+ Workspace crates:
26
+
27
+ - `jxcl-memory`
28
+ - `jxcl-endian`
29
+ - `jxcl-exceptions`
30
+ - `jxcl-types`
31
+
32
+ External crates:
33
+
34
+ *(none)*
35
+
36
+ ## Testing
37
+
38
+ `cargo test -p jxcl-load-store`: unit tests for unsigned roundtrip at
39
+ every width, correct sign extension on signed loads, truncation on
40
+ signed stores, `MemoryFault` -> `Exception` conversion on out-of-bounds
41
+ and misaligned access, and a property-style roundtrip sweep over several
42
+ representative values (including width extremes) for both unsigned and
43
+ signed 64-bit paths.
44
+
45
+ ## Status
46
+
47
+ `implemented`.
48
+
49
+ ## License
50
+
51
+ AGPLv3 / Commercial dual-license. See `LICENSE-AGPL`, `LICENSE-COMMERCIAL`, and `LICENSE-NOTICE` at the repository root.