October 2026 main drop: mirror from GitHub (part 2)
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- crates/jxcl-golden/tests/vectors/002_decrement.golden +6 -0
- crates/jxcl-hardware-test/Cargo.toml +15 -0
- crates/jxcl-hardware-test/README.md +72 -0
- crates/jxcl-hardware-test/src/lib.rs +107 -0
- crates/jxcl-hardware-test/tests/golden/core_datapath.v +237 -0
- crates/jxcl-hardware-test/tests/golden/core_datapath.vhdl +198 -0
- crates/jxcl-hardware-test/tests/golden/mux2.netlist.txt +1 -0
- crates/jxcl-hardware-test/tests/golden_regression.rs +66 -0
- crates/jxcl-hardware-test/tests/isa_conformance.rs +114 -0
- crates/jxcl-hardware/Cargo.toml +14 -0
- crates/jxcl-hardware/README.md +65 -0
- crates/jxcl-hardware/src/lib.rs +196 -0
- crates/jxcl-hdl/Cargo.toml +9 -0
- crates/jxcl-hdl/README.md +36 -0
- crates/jxcl-hdl/src/lib.rs +360 -0
- crates/jxcl-heap/Cargo.toml +15 -0
- crates/jxcl-heap/README.md +59 -0
- crates/jxcl-heap/src/lib.rs +244 -0
- crates/jxcl-http/Cargo.toml +15 -0
- crates/jxcl-http/README.md +45 -0
- crates/jxcl-http/src/lib.rs +137 -0
- crates/jxcl-instruction-validation/Cargo.toml +13 -0
- crates/jxcl-instruction-validation/README.md +35 -0
- crates/jxcl-instruction-validation/src/lib.rs +489 -0
- crates/jxcl-instructions/Cargo.toml +13 -0
- crates/jxcl-instructions/README.md +88 -0
- crates/jxcl-instructions/src/lib.rs +284 -0
- crates/jxcl-integration/Cargo.toml +12 -0
- crates/jxcl-integration/README.md +39 -0
- crates/jxcl-integration/src/lib.rs +23 -0
- crates/jxcl-interrupts/Cargo.toml +10 -0
- crates/jxcl-interrupts/README.md +37 -0
- crates/jxcl-interrupts/src/lib.rs +203 -0
- crates/jxcl-isa-metadata/Cargo.toml +10 -0
- crates/jxcl-isa-metadata/README.md +41 -0
- crates/jxcl-isa-metadata/src/lib.rs +231 -0
- crates/jxcl-isa-schema/Cargo.toml +14 -0
- crates/jxcl-isa-schema/README.md +41 -0
- crates/jxcl-isa-schema/src/lib.rs +306 -0
- crates/jxcl-isa-versioning/Cargo.toml +9 -0
- crates/jxcl-isa-versioning/README.md +44 -0
- crates/jxcl-isa-versioning/src/lib.rs +201 -0
- crates/jxcl-lexer/Cargo.toml +9 -0
- crates/jxcl-lexer/README.md +36 -0
- crates/jxcl-lexer/src/lib.rs +271 -0
- crates/jxcl-linker/Cargo.toml +13 -0
- crates/jxcl-linker/README.md +40 -0
- crates/jxcl-linker/src/lib.rs +335 -0
- crates/jxcl-load-store/Cargo.toml +15 -0
- 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.
|