diff --git a/.github/workflows/native-core.yml b/.github/workflows/native-core.yml new file mode 100644 index 0000000..aa0c6cc --- /dev/null +++ b/.github/workflows/native-core.yml @@ -0,0 +1,52 @@ +name: Native core gate + +on: + push: + branches: + - 'chatgpt/**' + paths: + - Cargo.toml + - Cargo.lock + - l64-native/** + - LOCUS64_*_RAIL.athens + - LOCUS64_*_CHANGE_CHAIN.athens + - .github/workflows/native-core.yml + pull_request: + branches: + - main + paths: + - Cargo.toml + - Cargo.lock + - l64-native/** + - LOCUS64_*_RAIL.athens + - LOCUS64_*_CHANGE_CHAIN.athens + - .github/workflows/native-core.yml + workflow_dispatch: + +permissions: + contents: read + +jobs: + verify: + if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install stable Rust + uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt, clippy + + - name: Cache Cargo + uses: Swatinem/rust-cache@v2 + + - name: Check formatting + run: cargo fmt --all --check + + - name: Test workspace + run: cargo test -q + + - name: Lint native core + run: cargo clippy -p l64-native --all-targets -- -D warnings diff --git a/Cargo.toml b/Cargo.toml index 3a7c67a..2f07a79 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,6 @@ [workspace] members = [ + "l64-native", "l64-core", "l64-locus", "l64-research", diff --git a/LOCUS64_CONSTRAINT_CORE_CHANGE_CHAIN.athens b/LOCUS64_CONSTRAINT_CORE_CHANGE_CHAIN.athens new file mode 100644 index 0000000..44d1a06 --- /dev/null +++ b/LOCUS64_CONSTRAINT_CORE_CHANGE_CHAIN.athens @@ -0,0 +1,39 @@ +ATHENS_DEVELOPMENT_RAIL v1 +field=key=projection_authority;value=non_authoritative +field=key=rail_version;value=6 +field=key=schema_version;value=1 +gate=id=constraint-core-closure-green +gate=id=context-consistency-green +gate=id=dimension-algebra-green +gate=id=guarded-obligation-green +gate=id=native-surface-green +stage=id=dimension-algebra +stage_field=stage=dimension-algebra;key=required_gates;value=dimension-algebra-green +stage_field=stage=dimension-algebra;key=status;value=complete +stage=id=context-consistency +stage_field=stage=context-consistency;key=depends_on;value=dimension-algebra +stage_field=stage=context-consistency;key=required_gates;value=context-consistency-green +stage_field=stage=context-consistency;key=status;value=complete +stage=id=guarded-obligation +stage_field=stage=guarded-obligation;key=depends_on;value=context-consistency +stage_field=stage=guarded-obligation;key=required_gates;value=guarded-obligation-green +stage_field=stage=guarded-obligation;key=status;value=complete +stage=id=native-surface +stage_field=stage=native-surface;key=depends_on;value=guarded-obligation +stage_field=stage=native-surface;key=required_gates;value=native-surface-green +stage_field=stage=native-surface;key=status;value=complete +stage=id=constraint-core-closure +stage_field=stage=constraint-core-closure;key=depends_on;value=native-surface +stage_field=stage=constraint-core-closure;key=required_gates;value=constraint-core-closure-green +stage_field=stage=constraint-core-closure;key=status;value=complete +history=from_status=current;gates=dimension-algebra-green;mode=linear_advance;stage_id=dimension-algebra;to_status=complete +history=evidence=local%3A22-tests%2Bclippy%3Bdimension-law%2Bdecoder-recheck;kind=dogfood_promotion_receipt;stage_id=dimension-algebra +history=from_status=current;gates=context-consistency-green;mode=linear_advance;stage_id=context-consistency;to_status=complete +history=evidence=local%3A24-tests%2Bclippy%3Bcontradiction-atomic%2Bdecode-recheck;kind=dogfood_promotion_receipt;stage_id=context-consistency +history=from_status=current;gates=guarded-obligation-green;mode=linear_advance;stage_id=guarded-obligation;to_status=complete +history=evidence=local%3A28-tests%2Bclippy%3Bsqrt-witness%7Cobligation%7Crejection%2Bforgery-refusal;kind=dogfood_promotion_receipt;stage_id=guarded-obligation +history=from_status=current;gates=native-surface-green;mode=linear_advance;stage_id=native-surface;to_status=complete +history=evidence=local%3A32-tests%2Bclippy%3Bconstraint-rna-dna-fixed-point%2Binvalid-refusal;kind=dogfood_promotion_receipt;stage_id=native-surface +history=from_status=current;gates=constraint-core-closure-green;mode=linear_advance;stage_id=constraint-core-closure;to_status=complete +history=evidence=github-cbcbfe5d9c4f34baf0ca54306c77a85ac970bd26-run-29990484222;kind=dogfood_promotion_receipt;stage_id=constraint-core-closure +END diff --git a/LOCUS64_EXECUTION_COHERENCE_RAIL.athens b/LOCUS64_EXECUTION_COHERENCE_RAIL.athens new file mode 100644 index 0000000..95053a1 --- /dev/null +++ b/LOCUS64_EXECUTION_COHERENCE_RAIL.athens @@ -0,0 +1,33 @@ +ATHENS_DEVELOPMENT_RAIL v1 +field=key=current_stage;value=proof-producing-congruence +field=key=next_stage;value=incremental-closure +field=key=projection_authority;value=non_authoritative +field=key=rail_version;value=2 +field=key=schema_version;value=1 +gate=id=incremental-closure-green +gate=id=legacy-authority-quarantine-green +gate=id=native-constraint-core-green +gate=id=native-upper-projection-green +gate=id=proof-producing-congruence-green +stage=id=native-constraint-core +stage_field=stage=native-constraint-core;key=required_gates;value=native-constraint-core-green +stage_field=stage=native-constraint-core;key=status;value=complete +stage=id=proof-producing-congruence +stage_field=stage=proof-producing-congruence;key=depends_on;value=native-constraint-core +stage_field=stage=proof-producing-congruence;key=required_gates;value=proof-producing-congruence-green +stage_field=stage=proof-producing-congruence;key=status;value=current +stage=id=incremental-closure +stage_field=stage=incremental-closure;key=depends_on;value=proof-producing-congruence +stage_field=stage=incremental-closure;key=required_gates;value=incremental-closure-green +stage_field=stage=incremental-closure;key=status;value=next +stage=id=native-upper-projection +stage_field=stage=native-upper-projection;key=depends_on;value=incremental-closure +stage_field=stage=native-upper-projection;key=required_gates;value=native-upper-projection-green +stage_field=stage=native-upper-projection;key=status;value=planned +stage=id=legacy-authority-quarantine +stage_field=stage=legacy-authority-quarantine;key=depends_on;value=native-upper-projection +stage_field=stage=legacy-authority-quarantine;key=required_gates;value=legacy-authority-quarantine-green +stage_field=stage=legacy-authority-quarantine;key=status;value=planned +history=from_status=current;gates=native-constraint-core-green;mode=linear_advance;stage_id=native-constraint-core;to_status=complete +history=evidence=github-cbcbfe5d9c4f34baf0ca54306c77a85ac970bd26-run-29990484222;kind=dogfood_promotion_receipt;stage_id=native-constraint-core +END diff --git a/LOCUS64_NATIVE_RAIL.athens b/LOCUS64_NATIVE_RAIL.athens new file mode 100644 index 0000000..7d4ce78 --- /dev/null +++ b/LOCUS64_NATIVE_RAIL.athens @@ -0,0 +1,25 @@ +ATHENS_DEVELOPMENT_RAIL v1 +field=key=projection_authority;value=non_authoritative +field=key=rail_version;value=8 +field=key=schema_version;value=1 +gate=id=legacy-demotion-green +gate=id=native-cli-membrane-green +gate=id=native-rna-pilot-green +stage=id=native-rna-pilot +stage_field=stage=native-rna-pilot;key=required_gates;value=native-rna-pilot-green +stage_field=stage=native-rna-pilot;key=status;value=complete +stage=id=native-cli-membrane +stage_field=stage=native-cli-membrane;key=depends_on;value=native-rna-pilot +stage_field=stage=native-cli-membrane;key=required_gates;value=native-cli-membrane-green +stage_field=stage=native-cli-membrane;key=status;value=complete +stage=id=legacy-demotion +stage_field=stage=legacy-demotion;key=depends_on;value=native-cli-membrane +stage_field=stage=legacy-demotion;key=required_gates;value=legacy-demotion-green +stage_field=stage=legacy-demotion;key=status;value=complete +history=from_status=current;gates=native-rna-pilot-green;mode=linear_advance;stage_id=native-rna-pilot;to_status=complete +history=evidence=github-ad80a8c344a92a857170994ac0dd0f52a41abfc5-run-29982693189;kind=dogfood_promotion_receipt;stage_id=native-rna-pilot +history=from_status=current;gates=native-cli-membrane-green;mode=linear_advance;stage_id=native-cli-membrane;to_status=complete +history=evidence=github-4c2a5fcaa50400e08e421c4a16ee55b662728fd4-run-29983336925;kind=dogfood_promotion_receipt;stage_id=native-cli-membrane +history=from_status=current;gates=legacy-demotion-green;mode=linear_advance;stage_id=legacy-demotion;to_status=complete +history=evidence=github-ce1dd6a3713d0dfaa3daf27b20e99116ebcc67aa-run-29983720943;kind=dogfood_promotion_receipt;stage_id=legacy-demotion +END diff --git a/l64-cli/Cargo.toml b/l64-cli/Cargo.toml index 6e1a8f1..20f6a04 100644 --- a/l64-cli/Cargo.toml +++ b/l64-cli/Cargo.toml @@ -2,6 +2,11 @@ name = "l64-cli" version = "0.1.0" edition = "2024" +autobins = false + +[[bin]] +name = "l64-cli" +path = "src/native_main.rs" [dependencies] anyhow = { workspace = true } @@ -19,6 +24,7 @@ l64-atlas = { path = "../l64-atlas" } l64-cert = { path = "../l64-cert" } l64-bundle = { path = "../l64-bundle" } l64-policy = { path = "../l64-policy" } +l64-native = { path = "../l64-native" } serde_json = { workspace = true } bincode = { workspace = true } hex = { workspace = true } diff --git a/l64-cli/src/native_main.rs b/l64-cli/src/native_main.rs new file mode 100644 index 0000000..ea97906 --- /dev/null +++ b/l64-cli/src/native_main.rs @@ -0,0 +1,28 @@ +mod native_membrane; + +mod legacy { + include!("main.rs"); + + pub(super) fn run() -> anyhow::Result<()> { + main() + } +} + +fn main() -> anyhow::Result<()> { + if native_membrane::is_legacy_child() { + native_membrane::announce_explicit_legacy(); + return legacy::run(); + } + if let Some(code) = native_membrane::run_explicit_legacy().map_err(anyhow::Error::msg)? { + if code == 0 { + return Ok(()); + } + std::process::exit(code); + } + if native_membrane::run_env().map_err(anyhow::Error::msg)? { + Ok(()) + } else { + native_membrane::warn_ambient_legacy_contact(); + legacy::run() + } +} diff --git a/l64-cli/src/native_membrane.rs b/l64-cli/src/native_membrane.rs new file mode 100644 index 0000000..ffb2698 --- /dev/null +++ b/l64-cli/src/native_membrane.rs @@ -0,0 +1,258 @@ +use std::{ + ffi::OsString, + fs, + io::Write, + path::{Path, PathBuf}, + process::Command, +}; + +const LEGACY_INGRESS_ENV: &str = "L64_EXPLICIT_LEGACY_INGRESS"; + +pub(super) fn is_legacy_child() -> bool { + std::env::var_os(LEGACY_INGRESS_ENV).is_some() +} + +pub(super) fn announce_explicit_legacy() { + eprintln!( + "legacy migration ingress: compatibility/forensic path; not current native authority" + ); +} + +pub(super) fn run_explicit_legacy() -> Result, String> { + let args = std::env::args_os().collect::>(); + if args.get(1).and_then(|item| item.to_str()) != Some("legacy") { + return Ok(None); + } + if args.len() == 2 || args.get(2).and_then(|item| item.to_str()) == Some("--help") { + println!( + "usage: l64-cli legacy ..." + ); + return Ok(Some(0)); + } + let command = args + .get(2) + .and_then(|item| item.to_str()) + .ok_or_else(|| "legacy ingress command must be UTF-8".to_string())?; + if !is_legacy_authority_command(command) { + return Err(format!( + "`{command}` is not a legacy RNA/DNA ingress command" + )); + } + let executable = std::env::current_exe().map_err(io_error)?; + Command::new(executable) + .env(LEGACY_INGRESS_ENV, "1") + .args(&args[2..]) + .status() + .map(|status| Some(status.code().unwrap_or(1))) + .map_err(io_error) +} + +pub(super) fn warn_ambient_legacy_contact() { + if let Some(command) = std::env::args() + .nth(1) + .filter(|command| is_legacy_authority_command(command)) + { + eprintln!( + "legacy migration ingress: ambient compatibility is deprecated; use `l64-cli legacy {command} ...`" + ); + } +} + +pub(super) fn run_env() -> Result { + let args = std::env::args_os().collect::>(); + let stdout = std::io::stdout(); + let mut stdout = stdout.lock(); + dispatch(&args, &mut stdout) +} + +pub(crate) fn dispatch(args: &[OsString], stdout: &mut impl Write) -> Result { + let Some(command) = args.get(1).and_then(|item| item.to_str()) else { + return Ok(false); + }; + let Some(file) = args.get(2).map(PathBuf::from) else { + return Ok(false); + }; + + match command { + "normalize-rna" => normalize(args, &file, stdout), + "compile-rna" => compile(args, &file, stdout), + "sequence-dna" => sequence(args, &file, stdout), + "verify-roundtrip" => verify(args, &file, stdout), + _ => Ok(false), + } +} + +fn normalize(args: &[OsString], file: &Path, stdout: &mut impl Write) -> Result { + let source = read(file)?; + if !is_native_rna(&source) { + return Ok(false); + } + require_arity(args, 3, "normalize-rna ")?; + let normalized = l64_native::normalize_rna(&source).map_err(native_error)?; + stdout.write_all(&normalized).map_err(io_error)?; + Ok(true) +} + +fn compile(args: &[OsString], file: &Path, stdout: &mut impl Write) -> Result { + let source = read(file)?; + if !is_native_rna(&source) { + return Ok(false); + } + let options = CompileOptions::parse(&args[3..])?; + let dna = l64_native::rna_to_dna(&source).map_err(native_error)?; + let out = options.out.unwrap_or_else(|| file.with_extension("dna")); + fs::write(&out, dna) + .map_err(|error| format!("failed to write `{}`: {error}", out.display()))?; + writeln!(stdout, "{}", out.display()).map_err(io_error)?; + Ok(true) +} + +fn sequence(args: &[OsString], file: &Path, stdout: &mut impl Write) -> Result { + let dna = read(file)?; + if !dna.starts_with(b"L64D") { + return Ok(false); + } + require_arity(args, 3, "sequence-dna ")?; + let rna = l64_native::dna_to_rna(&dna).map_err(native_error)?; + stdout.write_all(&rna).map_err(io_error)?; + Ok(true) +} + +fn verify(args: &[OsString], file: &Path, stdout: &mut impl Write) -> Result { + let source = read(file)?; + if !is_native_rna(&source) { + return Ok(false); + } + VerifyOptions::parse(&args[3..])?; + let first_dna = l64_native::rna_to_dna(&source).map_err(native_error)?; + let canonical_rna = l64_native::dna_to_rna(&first_dna).map_err(native_error)?; + let second_dna = l64_native::rna_to_dna(&canonical_rna).map_err(native_error)?; + if first_dna != second_dna { + return Err("native RNA/DNA fixed point failed".into()); + } + stdout + .write_all(b"native RNA/DNA fixed point verified\n") + .map_err(io_error)?; + Ok(true) +} + +#[derive(Default)] +struct CompileOptions { + out: Option, +} + +impl CompileOptions { + fn parse(args: &[OsString]) -> Result { + let mut options = Self::default(); + let mut index = 0; + while index < args.len() { + let argument = args[index].to_string_lossy(); + match argument.as_ref() { + "--out" => { + index += 1; + let value = args + .get(index) + .ok_or_else(|| "--out requires a path".to_string())?; + options.out = Some(PathBuf::from(value)); + } + value if value.starts_with("--out=") => { + options.out = Some(PathBuf::from(&value[6..])); + } + "--artifact-class" => { + index += 1; + let value = args + .get(index) + .and_then(|item| item.to_str()) + .ok_or_else(|| "--artifact-class requires a value".to_string())?; + require_gene(value)?; + } + value if value.starts_with("--artifact-class=") => { + require_gene(&value[17..])?; + } + "--persist-lineage" => { + return Err("--persist-lineage belongs to the legacy named-record path".into()); + } + _ => { + return Err(format!( + "unsupported native compile-rna option `{argument}`" + )); + } + } + index += 1; + } + Ok(options) + } +} + +struct VerifyOptions; + +impl VerifyOptions { + fn parse(args: &[OsString]) -> Result { + let mut index = 0; + while index < args.len() { + let argument = args[index].to_string_lossy(); + match argument.as_ref() { + "--artifact-class" => { + index += 1; + let value = args + .get(index) + .and_then(|item| item.to_str()) + .ok_or_else(|| "--artifact-class requires a value".to_string())?; + require_gene(value)?; + } + value if value.starts_with("--artifact-class=") => { + require_gene(&value[17..])?; + } + _ => { + return Err(format!( + "unsupported native verify-roundtrip option `{argument}`" + )); + } + } + index += 1; + } + Ok(Self) + } +} + +fn require_gene(value: &str) -> Result<(), String> { + if value == "gene" { + Ok(()) + } else { + Err("native RNA has structural routes, not a legacy artifact class".into()) + } +} + +fn require_arity(args: &[OsString], expected: usize, usage: &str) -> Result<(), String> { + if args.len() == expected { + Ok(()) + } else { + Err(format!("usage: l64-cli {usage}")) + } +} + +fn is_native_rna(source: &[u8]) -> bool { + source + .iter() + .position(|byte| !byte.is_ascii_whitespace()) + .is_some_and(|start| source[start..].starts_with(b"L64R1")) +} + +fn read(path: &Path) -> Result, String> { + fs::read(path).map_err(|error| format!("failed to read `{}`: {error}", path.display())) +} + +fn native_error(error: impl core::fmt::Debug) -> String { + format!("native authority rejected input: {error:?}") +} + +fn io_error(error: std::io::Error) -> String { + error.to_string() +} + +fn is_legacy_authority_command(command: &str) -> bool { + matches!( + command, + "normalize-rna" | "compile-rna" | "sequence-dna" | "inspect-dna" | "verify-roundtrip" + ) +} diff --git a/l64-cli/tests/native_cli.rs b/l64-cli/tests/native_cli.rs new file mode 100644 index 0000000..c4d6866 --- /dev/null +++ b/l64-cli/tests/native_cli.rs @@ -0,0 +1,154 @@ +use assert_cmd::Command; +use std::{ + fs, + path::PathBuf, + time::{SystemTime, UNIX_EPOCH}, +}; + +const SOURCE: &[u8] = b"L64R1 0x4c36344e41544956\na 1 0x41\na 2 0x42\na 3 0x43\nf 4 1 2\nf 5 2 3\nf 6 1 3\nv 7 4\nv 8 5\nc 9 7 8 6\n"; + +fn fixture(name: &str, bytes: &[u8]) -> PathBuf { + let dir = std::env::temp_dir().join(format!( + "l64_native_cli_{}", + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos() + )); + fs::create_dir_all(&dir).unwrap(); + let path = dir.join(name); + fs::write(&path, bytes).unwrap(); + path +} + +#[test] +fn native_compile_and_sequence_use_existing_commands() { + let rna = fixture("sample.rna", SOURCE); + let dna = rna.with_extension("dna"); + + Command::cargo_bin("l64-cli") + .unwrap() + .args([ + "compile-rna", + rna.to_str().unwrap(), + "--out", + dna.to_str().unwrap(), + ]) + .assert() + .success(); + assert!(fs::read(&dna).unwrap().starts_with(b"L64D")); + + let output = Command::cargo_bin("l64-cli") + .unwrap() + .args(["sequence-dna", dna.to_str().unwrap()]) + .output() + .unwrap(); + assert!(output.status.success()); + assert_eq!(output.stdout, SOURCE); +} + +#[test] +fn invalid_native_operation_cannot_create_dna() { + let source = b"L64R1 0x1\na 1 0x52\nm 2 1 2 3\nm 3 1 4 2\nm 4 1 2 2\nv 5 2\nv 6 3\nx 7 5 6 4\n"; + let rna = fixture("invalid.rna", source); + let dna = rna.with_extension("dna"); + + Command::cargo_bin("l64-cli") + .unwrap() + .args([ + "compile-rna", + rna.to_str().unwrap(), + "--out", + dna.to_str().unwrap(), + ]) + .assert() + .failure(); + assert!(!dna.exists()); +} + +#[test] +fn native_path_rejects_legacy_only_flags() { + let rna = fixture("sample.rna", SOURCE); + Command::cargo_bin("l64-cli") + .unwrap() + .args(["compile-rna", rna.to_str().unwrap(), "--persist-lineage"]) + .assert() + .failure(); + Command::cargo_bin("l64-cli") + .unwrap() + .args([ + "verify-roundtrip", + rna.to_str().unwrap(), + "--artifact-class", + "genome", + ]) + .assert() + .failure(); +} + +#[test] +fn non_native_commands_still_reach_legacy_dispatch() { + Command::cargo_bin("l64-cli") + .unwrap() + .arg("--help") + .assert() + .success(); +} + +#[test] +fn ambient_legacy_contact_is_visibly_demoted() { + let legacy = fixture("legacy.rna", "ι ≔ σ ‖ κ\n".as_bytes()); + let dna = legacy.with_extension("dna"); + let output = Command::cargo_bin("l64-cli") + .unwrap() + .args([ + "compile-rna", + legacy.to_str().unwrap(), + "--out", + dna.to_str().unwrap(), + ]) + .output() + .unwrap(); + assert!(output.status.success()); + assert!( + String::from_utf8_lossy(&output.stderr).contains("ambient compatibility is deprecated") + ); +} + +#[test] +fn explicit_legacy_ingress_reaches_unchanged_dispatcher() { + let legacy = fixture("legacy.rna", "ι ≔ σ ‖ κ\n".as_bytes()); + let dna = legacy.with_extension("dna"); + let output = Command::cargo_bin("l64-cli") + .unwrap() + .args([ + "legacy", + "compile-rna", + legacy.to_str().unwrap(), + "--out", + dna.to_str().unwrap(), + ]) + .output() + .unwrap(); + assert!(output.status.success()); + assert!(dna.exists()); + assert!(String::from_utf8_lossy(&output.stderr).contains("compatibility/forensic path")); +} + +#[test] +fn native_contact_does_not_emit_legacy_warning() { + let rna = fixture("native.rna", SOURCE); + let dna = rna.with_extension("dna"); + let output = Command::cargo_bin("l64-cli") + .unwrap() + .args([ + "compile-rna", + rna.to_str().unwrap(), + "--out", + dna.to_str().unwrap(), + ]) + .output() + .unwrap(); + assert!(output.status.success()); + assert!(!String::from_utf8_lossy(&output.stderr).contains("legacy migration ingress")); +} diff --git a/l64-native/Cargo.toml b/l64-native/Cargo.toml new file mode 100644 index 0000000..dcd981a --- /dev/null +++ b/l64-native/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "l64-native" +edition.workspace = true +version.workspace = true +license.workspace = true + +[dependencies] +blake3.workspace = true diff --git a/l64-native/README.md b/l64-native/README.md new file mode 100644 index 0000000..cdbf54b --- /dev/null +++ b/l64-native/README.md @@ -0,0 +1,44 @@ +# l64-native + +A deliberately small, additive execution spine for Locus64. + +This crate is not a projection of the legacy `QaEntry` / `RegistryBundle` ontology. It carries: + +- composed numeric routes; +- one node algebra for types, values, constraints, operations, judgments, witnesses, and obligations; +- ordered compact ports; +- persistent context deltas with contradiction checks; +- one transition journal; +- one canonical structural codec; +- one transactional admission path. + +The first workload boundary proves that lawful typed function composition commits while incompatible matrix multiplication is rejected without state mutation. + +The second boundary adds a bounded native decoder. Canonical bytes must decode, satisfy structural laws, and re-encode byte-for-byte. The decoder rejects malformed node order, invalid port laws, bad contexts, invalid route coverage, unknown opcodes, structural overrun, trailing bytes, and non-canonical ordering. + +The third boundary makes primitive execution proof-carrying without adding a receipt schema. Every admitted operation receives a judgment type and kernel witness at routes deterministically composed from the operation route. Generic value insertion cannot construct a witness for a kernel-only judgment. + +The fourth boundary adds a native DNA frame with a fixed 44-byte binary header, bounded canonical payload, embedded domain-separated BLAKE3 commitment, and exact DNA decode/re-encode fixed point. The frame contains no string metadata or legacy record payload. + +The fifth boundary adds a compact authored RNA ingress. `L64R1` uses one declared domain and strictly increasing numeric local slots; routes are composed as `(domain, slot)`. Its byte-oriented instructions lower directly through the existing graph and transaction APIs. Sequencing omits intrinsic evidence nodes because their routes and structure are deterministically derived. + +The sixth boundary adds the first native constraint core without creating a parallel schema system: + +- seven-axis signed dimensions packed into one 64-bit value; +- quantity types over existing carrier types; +- dimension-checked addition, multiplication, division, and square root; +- compact constraint nodes and persistent context deltas; +- contradiction rejection before context or journal mutation; +- a guarded square-root operation that yields a kernel witness when proven, rejects when refuted, and yields a native obligation when unresolved; +- decoder-side re-execution of operation and evidence law, preventing structurally plausible forged authority; +- canonical `L64R1 → L64D → L64R1 → L64D` fixed points for both discharged and unresolved guards. + +`LOCUS64_CONSTRAINT_CORE_CHANGE_CHAIN.athens` is the promotion boundary for this sixth stage: dimension algebra, context consistency, guarded obligation, native surface, then full closure. The parent execution rail cannot advance until that chain closes on repository evidence. + +The larger implementation files are factored only at existing item boundaries into construction, typing, transaction, validation, codec, and RNA concerns. This changes review locality without introducing another authority layer or altering canonical bytes. + +State identity is the domain-separated BLAKE3 commitment of canonical native bytes. No native name, claim identifier, theorem identifier, campaign identifier, JSON field name, or generic serialization schema participates. + +The existing `l64-cli` command names now route `L64R1` and `L64D` directly through this native path. Legacy RNA/DNA behavior is classified as compatibility/forensic ingress and is available explicitly through `l64-cli legacy ...`; ambient fallback remains temporarily available with a mandatory deprecation warning. + +This remains additive. It does not yet implement proof-producing congruence, incremental dependency closure, native upper-stack projections, or replacement of the legacy runtime, registry, certification, and old packet implementation internally. diff --git a/l64-native/src/codec.rs b/l64-native/src/codec.rs new file mode 100644 index 0000000..8b3b6cf --- /dev/null +++ b/l64-native/src/codec.rs @@ -0,0 +1,38 @@ +use std::collections::BTreeMap; + +use crate::kernel::{EVIDENCE_LOCUS, JUDGMENT_LOCUS}; +use crate::{ContextDelta, Graph, LocusWord, Node, NodeId, OpCode, Port, PortRole, Route}; + +const CODEC_VERSION: u16 = 3; +const COMMITMENT_DOMAIN: &[u8] = b"l64-native-state-v3\0"; +const MAX_NODES: usize = 1 << 20; +const MAX_PORTS: usize = 1 << 22; +const MAX_CONTEXTS: usize = 1 << 20; +const MAX_ROUTES: usize = 1 << 20; +const MAX_ROUTE_WORDS: usize = 64; +const NO_NODE: NodeId = u32::MAX; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DecodeError { + Truncated, + BadMagic, + UnsupportedVersion { version: u16 }, + StructuralBound, + UnknownOpcode { opcode: u16 }, + UnknownPortRole { role: u8 }, + NonZeroPortFlags { flags: u8 }, + InvalidNode, + InvalidPortRange, + InvalidPortTarget, + InvalidPortLaw, + InvalidContext, + InvalidRoute, + InvalidAuthority, + DuplicateRoute, + TrailingBytes, + NonCanonical, +} + +include!("codec/io.rs"); +include!("codec/validation.rs"); +include!("codec/reader.rs"); diff --git a/l64-native/src/codec/io.rs b/l64-native/src/codec/io.rs new file mode 100644 index 0000000..5820083 --- /dev/null +++ b/l64-native/src/codec/io.rs @@ -0,0 +1,142 @@ +pub fn canonical_bytes(graph: &Graph) -> Vec { + let mut out = + Vec::with_capacity(22 + graph.nodes_raw().len() * 24 + graph.ports_raw().len() * 8); + out.extend_from_slice(b"L64N"); + out.extend_from_slice(&CODEC_VERSION.to_le_bytes()); + push_len(&mut out, graph.nodes_raw().len()); + push_len(&mut out, graph.ports_raw().len()); + push_len(&mut out, graph.contexts_raw().len()); + push_len(&mut out, graph.routes_raw().len()); + + for node in graph.nodes_raw() { + out.extend_from_slice(&(node.opcode() as u16).to_le_bytes()); + out.extend_from_slice(&node.context().to_le_bytes()); + out.extend_from_slice(&node.ty().unwrap_or(NO_NODE).to_le_bytes()); + out.extend_from_slice(&node.payload().to_le_bytes()); + let range = node.port_range(); + out.extend_from_slice(&(range.start as u32).to_le_bytes()); + out.extend_from_slice(&((range.end - range.start) as u16).to_le_bytes()); + } + + for port in graph.ports_raw() { + out.extend_from_slice(&port.target().to_le_bytes()); + out.push(port.role() as u8); + out.push(port.flags()); + out.extend_from_slice(&port.ordinal().to_le_bytes()); + } + + for context in graph.contexts_raw() { + out.extend_from_slice(&context.parent().to_le_bytes()); + out.extend_from_slice(&context.binding().unwrap_or(NO_NODE).to_le_bytes()); + } + + for (route, node) in graph.routes_raw() { + out.extend_from_slice(&route.domain().0.to_le_bytes()); + push_len(&mut out, route.tail().len()); + for word in route.tail() { + out.extend_from_slice(&word.0.to_le_bytes()); + } + out.extend_from_slice(&node.to_le_bytes()); + } + + out +} + +pub fn decode_canonical(bytes: &[u8]) -> Result { + let mut reader = Reader::new(bytes); + if reader.take(4)? != b"L64N" { + return Err(DecodeError::BadMagic); + } + let version = reader.u16()?; + if version != CODEC_VERSION { + return Err(DecodeError::UnsupportedVersion { version }); + } + + let node_count = reader.count(MAX_NODES)?; + let port_count = reader.count(MAX_PORTS)?; + let context_count = reader.count(MAX_CONTEXTS)?; + let route_count = reader.count(MAX_ROUTES)?; + if context_count == 0 || route_count != node_count { + return Err(DecodeError::InvalidContext); + } + + reader.require(node_count, 24)?; + let mut nodes = Vec::with_capacity(node_count); + for _ in 0..node_count { + let raw_opcode = reader.u16()?; + let opcode = OpCode::from_raw(raw_opcode) + .filter(|opcode| opcode.is_persisted_node()) + .ok_or(DecodeError::UnknownOpcode { opcode: raw_opcode })?; + let context = reader.u32()?; + let ty = reader.u32()?; + let payload = reader.u64()?; + let first_port = reader.u32()?; + let port_count = reader.u16()?; + nodes.push(Node::from_raw( + payload, context, ty, first_port, opcode, port_count, + )); + } + + reader.require(port_count, 8)?; + let mut ports = Vec::with_capacity(port_count); + for _ in 0..port_count { + let target = reader.u32()?; + let raw_role = reader.u8()?; + let role = + PortRole::from_raw(raw_role).ok_or(DecodeError::UnknownPortRole { role: raw_role })?; + let flags = reader.u8()?; + if flags != 0 { + return Err(DecodeError::NonZeroPortFlags { flags }); + } + let ordinal = reader.u16()?; + ports.push(Port::from_raw(target, role, flags, ordinal)); + } + + reader.require(context_count, 8)?; + let mut contexts = Vec::with_capacity(context_count); + for _ in 0..context_count { + contexts.push(ContextDelta::from_raw(reader.u32()?, reader.u32()?)); + } + + let mut routes = BTreeMap::new(); + for _ in 0..route_count { + let domain = LocusWord(reader.u64()?); + let tail_len = reader.count(MAX_ROUTE_WORDS)?; + reader.require(tail_len, 8)?; + let mut tail = Vec::with_capacity(tail_len); + for _ in 0..tail_len { + tail.push(LocusWord(reader.u64()?)); + } + let node = reader.u32()?; + let route = Route::from_parts(domain, tail.into_boxed_slice()); + if routes.insert(route, node).is_some() { + return Err(DecodeError::DuplicateRoute); + } + } + + if !reader.is_empty() { + return Err(DecodeError::TrailingBytes); + } + + validate_structure(&nodes, &ports, &contexts, &routes)?; + let commitment = commitment_bytes(bytes); + let graph = Graph::from_decoded_parts(nodes, ports, contexts, routes, commitment); + graph + .validate_decoded_authority() + .map_err(|_| DecodeError::InvalidAuthority)?; + if canonical_bytes(&graph) != bytes { + return Err(DecodeError::NonCanonical); + } + Ok(graph) +} + +pub(crate) fn state_commitment(graph: &Graph) -> [u8; 32] { + commitment_bytes(&canonical_bytes(graph)) +} + +fn commitment_bytes(bytes: &[u8]) -> [u8; 32] { + let mut hasher = blake3::Hasher::new(); + hasher.update(COMMITMENT_DOMAIN); + hasher.update(bytes); + *hasher.finalize().as_bytes() +} diff --git a/l64-native/src/codec/reader.rs b/l64-native/src/codec/reader.rs new file mode 100644 index 0000000..f2127e2 --- /dev/null +++ b/l64-native/src/codec/reader.rs @@ -0,0 +1,69 @@ +fn push_len(out: &mut Vec, len: usize) { + let value = u32::try_from(len).expect("native graph section exceeds u32 length"); + out.extend_from_slice(&value.to_le_bytes()); +} + +struct Reader<'a> { + bytes: &'a [u8], + cursor: usize, +} + +impl<'a> Reader<'a> { + fn new(bytes: &'a [u8]) -> Self { + Self { bytes, cursor: 0 } + } + + fn is_empty(&self) -> bool { + self.cursor == self.bytes.len() + } + + fn take(&mut self, len: usize) -> Result<&'a [u8], DecodeError> { + let end = self.cursor.checked_add(len).ok_or(DecodeError::Truncated)?; + let value = self + .bytes + .get(self.cursor..end) + .ok_or(DecodeError::Truncated)?; + self.cursor = end; + Ok(value) + } + + fn require(&self, count: usize, width: usize) -> Result<(), DecodeError> { + let bytes = count + .checked_mul(width) + .ok_or(DecodeError::StructuralBound)?; + if self.bytes.len().saturating_sub(self.cursor) < bytes { + return Err(DecodeError::Truncated); + } + Ok(()) + } + + fn count(&mut self, maximum: usize) -> Result { + let count = self.u32()? as usize; + if count > maximum { + return Err(DecodeError::StructuralBound); + } + Ok(count) + } + + fn u8(&mut self) -> Result { + Ok(self.take(1)?[0]) + } + + fn u16(&mut self) -> Result { + Ok(u16::from_le_bytes( + self.take(2)?.try_into().expect("fixed-width read"), + )) + } + + fn u32(&mut self) -> Result { + Ok(u32::from_le_bytes( + self.take(4)?.try_into().expect("fixed-width read"), + )) + } + + fn u64(&mut self) -> Result { + Ok(u64::from_le_bytes( + self.take(8)?.try_into().expect("fixed-width read"), + )) + } +} diff --git a/l64-native/src/codec/validation.rs b/l64-native/src/codec/validation.rs new file mode 100644 index 0000000..1ae5755 --- /dev/null +++ b/l64-native/src/codec/validation.rs @@ -0,0 +1,219 @@ +fn validate_structure( + nodes: &[Node], + ports: &[Port], + contexts: &[ContextDelta], + routes: &BTreeMap, +) -> Result<(), DecodeError> { + let root = contexts.first().ok_or(DecodeError::InvalidContext)?; + if root.parent() != 0 || root.binding().is_some() { + return Err(DecodeError::InvalidContext); + } + for (index, context) in contexts.iter().enumerate().skip(1) { + if context.parent() as usize >= index { + return Err(DecodeError::InvalidContext); + } + let binding = context.binding().ok_or(DecodeError::InvalidContext)?; + if binding as usize >= nodes.len() { + return Err(DecodeError::InvalidContext); + } + } + + let mut port_cursor = 0usize; + for (index, node) in nodes.iter().enumerate() { + if node.context() as usize >= contexts.len() { + return Err(DecodeError::InvalidNode); + } + if let Some(ty) = node.ty() { + if ty as usize >= index || !nodes[ty as usize].opcode().is_type() { + return Err(DecodeError::InvalidNode); + } + } else if !node.opcode().is_untyped_authority() { + return Err(DecodeError::InvalidNode); + } + + let range = node.port_range(); + if range.start != port_cursor || range.end > ports.len() { + return Err(DecodeError::InvalidPortRange); + } + let node_ports = &ports[range.clone()]; + for (ordinal, port) in node_ports.iter().enumerate() { + if port.target() as usize >= index { + return Err(DecodeError::InvalidPortTarget); + } + if port.ordinal() as usize != ordinal { + return Err(DecodeError::InvalidPortLaw); + } + } + validate_port_law(node.opcode(), node_ports, node.ty().is_some())?; + validate_node_semantics(index, node, node_ports, nodes, ports)?; + port_cursor = range.end; + } + if port_cursor != ports.len() { + return Err(DecodeError::InvalidPortRange); + } + + let mut covered = vec![false; nodes.len()]; + for node in routes.values().copied() { + let slot = covered + .get_mut(node as usize) + .ok_or(DecodeError::InvalidRoute)?; + if *slot { + return Err(DecodeError::InvalidRoute); + } + *slot = true; + } + if covered.iter().any(|covered| !covered) { + return Err(DecodeError::InvalidRoute); + } + validate_evidence_routes(nodes, routes)?; + Ok(()) +} + +fn validate_node_semantics( + index: usize, + node: &Node, + node_ports: &[Port], + nodes: &[Node], + ports: &[Port], +) -> Result<(), DecodeError> { + match node.opcode() { + OpCode::Value => { + let ty = node.ty().ok_or(DecodeError::InvalidNode)?; + if nodes[ty as usize].opcode() == OpCode::TypeJudgment { + return Err(DecodeError::InvalidNode); + } + } + OpCode::KernelWitness | OpCode::Obligation => { + let ty = node.ty().ok_or(DecodeError::InvalidNode)?; + if nodes[ty as usize].opcode() != OpCode::TypeJudgment { + return Err(DecodeError::InvalidNode); + } + if node.opcode() == OpCode::Obligation + && crate::ConstraintKind::from_payload(node.payload()).is_none() + { + return Err(DecodeError::InvalidNode); + } + } + OpCode::TypeQuantity => { + if crate::Dimension::from_bits(node.payload()).is_none() { + return Err(DecodeError::InvalidNode); + } + } + OpCode::Constraint => { + if crate::ConstraintKind::from_payload(node.payload()).is_none() { + return Err(DecodeError::InvalidNode); + } + } + OpCode::TypeJudgment => { + let rule = OpCode::from_raw(node.payload() as u16) + .filter(|opcode| opcode.is_executable()) + .ok_or(DecodeError::InvalidNode)?; + if node_ports.len() < 3 { + return Err(DecodeError::InvalidNode); + } + let subject = node_ports[0].target() as usize; + if subject >= index || nodes[subject].opcode() != rule { + return Err(DecodeError::InvalidNode); + } + let subject_node = &nodes[subject]; + let conclusion = node_ports.last().ok_or(DecodeError::InvalidNode)?.target(); + if subject_node.ty() != Some(conclusion) { + return Err(DecodeError::InvalidNode); + } + let subject_ports = ports + .get(subject_node.port_range()) + .ok_or(DecodeError::InvalidPortRange)?; + if subject_ports.len() != node_ports.len() - 2 + || subject_ports + .iter() + .map(Port::target) + .ne(node_ports[1..node_ports.len() - 1].iter().map(Port::target)) + { + return Err(DecodeError::InvalidNode); + } + } + _ => {} + } + Ok(()) +} + +fn validate_evidence_routes( + nodes: &[Node], + routes: &BTreeMap, +) -> Result<(), DecodeError> { + let mut attached = vec![false; nodes.len()]; + for (route, node_id) in routes { + let node = &nodes[*node_id as usize]; + if !node.opcode().is_executable() { + continue; + } + let judgment = *routes + .get(&route.composed(JUDGMENT_LOCUS)) + .ok_or(DecodeError::InvalidRoute)?; + let witness = *routes + .get(&route.composed(EVIDENCE_LOCUS)) + .ok_or(DecodeError::InvalidRoute)?; + if nodes[judgment as usize].opcode() != OpCode::TypeJudgment + || !matches!( + nodes[witness as usize].opcode(), + OpCode::KernelWitness | OpCode::Obligation + ) + || nodes[witness as usize].ty() != Some(judgment) + { + return Err(DecodeError::InvalidRoute); + } + attached[judgment as usize] = true; + attached[witness as usize] = true; + } + for (index, node) in nodes.iter().enumerate() { + if matches!( + node.opcode(), + OpCode::TypeJudgment | OpCode::KernelWitness | OpCode::Obligation + ) && !attached[index] + { + return Err(DecodeError::InvalidRoute); + } + } + Ok(()) +} + +fn validate_port_law(opcode: OpCode, ports: &[Port], has_type: bool) -> Result<(), DecodeError> { + let valid = match opcode { + OpCode::TypeAtom => ports.is_empty() && !has_type, + OpCode::TypeMatrix => { + ports.len() == 1 && ports[0].role() == PortRole::Parameter && !has_type + } + OpCode::TypeFunction => { + ports.len() == 2 + && ports[0].role() == PortRole::Domain + && ports[1].role() == PortRole::Codomain + && !has_type + } + OpCode::TypeQuantity => { + ports.len() == 1 && ports[0].role() == PortRole::Parameter && !has_type + } + OpCode::Constraint => ports.len() == 1 && ports[0].role() == PortRole::Subject && !has_type, + OpCode::Value => ports.is_empty() && has_type, + OpCode::Compose | OpCode::MatMul | OpCode::Add | OpCode::Multiply | OpCode::Divide => { + ports.len() == 2 + && ports.iter().all(|port| port.role() == PortRole::Argument) + && has_type + } + OpCode::Sqrt => ports.len() == 1 && ports[0].role() == PortRole::Argument && has_type, + OpCode::TypeJudgment => { + ports.len() >= 3 + && ports[0].role() == PortRole::Subject + && ports[1..ports.len() - 1] + .iter() + .all(|port| port.role() == PortRole::Premise) + && ports + .last() + .is_some_and(|port| port.role() == PortRole::Conclusion) + && !has_type + } + OpCode::KernelWitness => ports.is_empty() && has_type, + OpCode::Obligation => ports.len() == 1 && ports[0].role() == PortRole::Premise && has_type, + OpCode::ExtendContext => false, + }; + valid.then_some(()).ok_or(DecodeError::InvalidPortLaw) +} diff --git a/l64-native/src/context.rs b/l64-native/src/context.rs new file mode 100644 index 0000000..3d02fed --- /dev/null +++ b/l64-native/src/context.rs @@ -0,0 +1,22 @@ +use crate::{ContextId, NodeId}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(C)] +pub struct ContextDelta { + pub(crate) parent: ContextId, + pub(crate) binding: NodeId, +} + +impl ContextDelta { + pub(crate) fn from_raw(parent: ContextId, binding: NodeId) -> Self { + Self { parent, binding } + } + + pub fn parent(&self) -> ContextId { + self.parent + } + + pub fn binding(&self) -> Option { + (self.binding != u32::MAX).then_some(self.binding) + } +} diff --git a/l64-native/src/dimension.rs b/l64-native/src/dimension.rs new file mode 100644 index 0000000..922d2cf --- /dev/null +++ b/l64-native/src/dimension.rs @@ -0,0 +1,66 @@ +const AXIS_COUNT: usize = 7; +const USED_BITS: u32 = (AXIS_COUNT as u32) * 8; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(transparent)] +pub struct Dimension(u64); + +impl Dimension { + pub const DIMENSIONLESS: Self = Self(0); + pub const LENGTH: Self = Self(1); + pub const MASS: Self = Self(1 << 8); + pub const TIME: Self = Self(1 << 16); + + pub fn new(exponents: [i8; AXIS_COUNT]) -> Self { + let mut bits = 0_u64; + for (axis, exponent) in exponents.into_iter().enumerate() { + bits |= u64::from(exponent as u8) << (axis * 8); + } + Self(bits) + } + + pub fn exponents(self) -> [i8; AXIS_COUNT] { + let mut exponents = [0_i8; AXIS_COUNT]; + for (axis, exponent) in exponents.iter_mut().enumerate() { + *exponent = ((self.0 >> (axis * 8)) as u8) as i8; + } + exponents + } + + pub fn bits(self) -> u64 { + self.0 + } + + pub(crate) fn from_bits(bits: u64) -> Option { + (bits >> USED_BITS == 0).then_some(Self(bits)) + } + + pub fn multiply(self, other: Self) -> Option { + self.combine(other, i8::checked_add) + } + + pub fn divide(self, other: Self) -> Option { + self.combine(other, i8::checked_sub) + } + + pub fn square_root(self) -> Option { + let mut result = [0_i8; AXIS_COUNT]; + for (axis, exponent) in self.exponents().into_iter().enumerate() { + if exponent % 2 != 0 { + return None; + } + result[axis] = exponent / 2; + } + Some(Self::new(result)) + } + + fn combine(self, other: Self, operation: fn(i8, i8) -> Option) -> Option { + let left = self.exponents(); + let right = other.exponents(); + let mut result = [0_i8; AXIS_COUNT]; + for axis in 0..AXIS_COUNT { + result[axis] = operation(left[axis], right[axis])?; + } + Some(Self::new(result)) + } +} diff --git a/l64-native/src/dna.rs b/l64-native/src/dna.rs new file mode 100644 index 0000000..55717cd --- /dev/null +++ b/l64-native/src/dna.rs @@ -0,0 +1,93 @@ +use crate::{DecodeError, Graph, canonical_bytes, decode_canonical}; + +const DNA_MAGIC: &[u8; 4] = b"L64D"; +const DNA_VERSION: u16 = 1; +const DNA_FLAGS: u16 = 0; +const DNA_COMMITMENT_DOMAIN: &[u8] = b"l64-native-dna-v1\0"; +const DNA_HEADER_BYTES: usize = 44; +pub const MAX_NATIVE_DNA_PAYLOAD_BYTES: usize = 1 << 28; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DnaError { + PayloadTooLarge, + Truncated, + BadMagic, + UnsupportedVersion { version: u16 }, + UnsupportedFlags { flags: u16 }, + TrailingBytes, + CommitmentMismatch, + Canonical(DecodeError), +} + +impl From for DnaError { + fn from(value: DecodeError) -> Self { + Self::Canonical(value) + } +} + +pub fn dna_bytes(graph: &Graph) -> Result, DnaError> { + let payload = canonical_bytes(graph); + if payload.len() > MAX_NATIVE_DNA_PAYLOAD_BYTES || payload.len() > u32::MAX as usize { + return Err(DnaError::PayloadTooLarge); + } + + let mut out = Vec::with_capacity(DNA_HEADER_BYTES + payload.len()); + out.extend_from_slice(DNA_MAGIC); + out.extend_from_slice(&DNA_VERSION.to_le_bytes()); + out.extend_from_slice(&DNA_FLAGS.to_le_bytes()); + out.extend_from_slice(&(payload.len() as u32).to_le_bytes()); + out.extend_from_slice(&dna_commitment(&payload)); + out.extend_from_slice(&payload); + Ok(out) +} + +pub fn decode_dna(bytes: &[u8]) -> Result { + if bytes.len() < DNA_HEADER_BYTES { + return Err(DnaError::Truncated); + } + if &bytes[..4] != DNA_MAGIC { + return Err(DnaError::BadMagic); + } + + let version = u16::from_le_bytes(bytes[4..6].try_into().expect("fixed DNA version field")); + if version != DNA_VERSION { + return Err(DnaError::UnsupportedVersion { version }); + } + let flags = u16::from_le_bytes(bytes[6..8].try_into().expect("fixed DNA flags field")); + if flags != DNA_FLAGS { + return Err(DnaError::UnsupportedFlags { flags }); + } + let payload_len = + u32::from_le_bytes(bytes[8..12].try_into().expect("fixed DNA length field")) as usize; + if payload_len > MAX_NATIVE_DNA_PAYLOAD_BYTES { + return Err(DnaError::PayloadTooLarge); + } + + let expected_len = DNA_HEADER_BYTES + .checked_add(payload_len) + .ok_or(DnaError::PayloadTooLarge)?; + if bytes.len() < expected_len { + return Err(DnaError::Truncated); + } + if bytes.len() > expected_len { + return Err(DnaError::TrailingBytes); + } + + let stored_commitment: [u8; 32] = bytes[12..44] + .try_into() + .expect("fixed DNA commitment field"); + let payload = &bytes[DNA_HEADER_BYTES..]; + let computed_commitment = dna_commitment(payload); + if stored_commitment != computed_commitment { + return Err(DnaError::CommitmentMismatch); + } + + Ok(decode_canonical(payload)?) +} + +fn dna_commitment(payload: &[u8]) -> [u8; 32] { + let mut hasher = blake3::Hasher::new(); + hasher.update(DNA_COMMITMENT_DOMAIN); + hasher.update(payload); + *hasher.finalize().as_bytes() +} diff --git a/l64-native/src/graph.rs b/l64-native/src/graph.rs new file mode 100644 index 0000000..7ca66d3 --- /dev/null +++ b/l64-native/src/graph.rs @@ -0,0 +1,89 @@ +use std::collections::BTreeMap; + +use crate::kernel::{ConstraintState, EvidencePlan}; +use crate::{ + ConstraintKind, ContextDelta, Dimension, JournalEvent, LocusWord, Obstruction, OpCode, Port, + PortRole, Route, +}; + +pub type NodeId = u32; +pub type ContextId = u32; +pub type EventId = u32; + +pub const ROOT_CONTEXT: ContextId = 0; +const NO_NODE: NodeId = u32::MAX; +const META_TYPE: NodeId = u32::MAX; +const NO_EVENT: EventId = u32::MAX; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(C)] +pub struct Node { + payload: u64, + context: ContextId, + ty: NodeId, + first_port: u32, + opcode: u16, + port_count: u16, +} + +impl Node { + pub(crate) fn from_raw( + payload: u64, + context: ContextId, + ty: NodeId, + first_port: u32, + opcode: OpCode, + port_count: u16, + ) -> Self { + Self { + payload, + context, + ty, + first_port, + opcode: opcode as u16, + port_count, + } + } + + pub fn opcode(&self) -> OpCode { + OpCode::from_raw(self.opcode).expect("stored opcode is validated at insertion") + } + + pub fn context(&self) -> ContextId { + self.context + } + + pub fn ty(&self) -> Option { + (self.ty != META_TYPE).then_some(self.ty) + } + + pub fn payload(&self) -> u64 { + self.payload + } + + pub fn port_range(&self) -> core::ops::Range { + let start = self.first_port as usize; + start..start + self.port_count as usize + } +} + +#[derive(Debug, Clone)] +pub struct Graph { + nodes: Vec, + ports: Vec, + contexts: Vec, + routes: BTreeMap, + journal: Vec, + commitment: [u8; 32], +} + +impl Default for Graph { + fn default() -> Self { + Self::new() + } +} + +include!("graph/construction.rs"); +include!("graph/typing.rs"); +include!("graph/storage.rs"); +include!("graph/context.rs"); diff --git a/l64-native/src/graph/construction.rs b/l64-native/src/graph/construction.rs new file mode 100644 index 0000000..1a99d5c --- /dev/null +++ b/l64-native/src/graph/construction.rs @@ -0,0 +1,277 @@ +impl Graph { + pub(crate) fn from_decoded_parts( + nodes: Vec, + ports: Vec, + contexts: Vec, + routes: BTreeMap, + commitment: [u8; 32], + ) -> Self { + Self { + nodes, + ports, + contexts, + routes, + journal: Vec::new(), + commitment, + } + } + + pub fn new() -> Self { + let mut graph = Self { + nodes: Vec::new(), + ports: Vec::new(), + contexts: vec![ContextDelta::root()], + routes: BTreeMap::new(), + journal: Vec::new(), + commitment: [0; 32], + }; + graph.commitment = crate::codec::state_commitment(&graph); + graph + } + + pub fn node(&self, id: NodeId) -> Option<&Node> { + self.nodes.get(id as usize) + } + + pub fn ports(&self, node: NodeId) -> Option<&[Port]> { + let node = self.node(node)?; + self.ports.get(node.port_range()) + } + + pub fn resolve(&self, route: &Route) -> Option { + self.routes.get(route).copied() + } + + pub fn node_count(&self) -> usize { + self.nodes.len() + } + + pub fn port_count(&self) -> usize { + self.ports.len() + } + + pub fn context_count(&self) -> usize { + self.contexts.len() + } + + pub fn journal_len(&self) -> usize { + self.journal.len() + } + + pub fn state_commitment(&self) -> [u8; 32] { + self.commitment + } + + pub fn journal(&self) -> &[JournalEvent] { + &self.journal + } + + pub fn extend_context( + &mut self, + parent: ContextId, + binding: NodeId, + ) -> Result { + self.ensure_context(parent)?; + self.ensure_node(binding)?; + self.validate_context_binding(parent, binding)?; + let before = self.commitment; + let id = self.contexts.len() as ContextId; + self.contexts.push(ContextDelta { parent, binding }); + let after = crate::codec::state_commitment(self); + self.push_event(OpCode::ExtendContext, binding, before, after); + self.commitment = after; + Ok(id) + } + + pub fn declare_atom_type( + &mut self, + route: Route, + atom_code: LocusWord, + ) -> Result { + self.insert_committed_node( + route, + ROOT_CONTEXT, + META_TYPE, + OpCode::TypeAtom, + atom_code.0, + &[], + ) + } + + pub fn declare_matrix_type( + &mut self, + route: Route, + element: NodeId, + rows: u32, + cols: u32, + ) -> Result { + self.ensure_type(element)?; + let payload = u64::from(rows) << 32 | u64::from(cols); + let port = Port::new(element, PortRole::Parameter, 0); + self.insert_committed_node( + route, + ROOT_CONTEXT, + META_TYPE, + OpCode::TypeMatrix, + payload, + &[port], + ) + } + + pub fn declare_function_type( + &mut self, + route: Route, + domain: NodeId, + codomain: NodeId, + ) -> Result { + self.ensure_type(domain)?; + self.ensure_type(codomain)?; + let ports = [ + Port::new(domain, PortRole::Domain, 0), + Port::new(codomain, PortRole::Codomain, 1), + ]; + self.insert_committed_node( + route, + ROOT_CONTEXT, + META_TYPE, + OpCode::TypeFunction, + 0, + &ports, + ) + } + + pub fn declare_quantity_type( + &mut self, + route: Route, + carrier: NodeId, + dimension: Dimension, + ) -> Result { + self.ensure_type(carrier)?; + let port = Port::new(carrier, PortRole::Parameter, 0); + self.insert_committed_node( + route, + ROOT_CONTEXT, + META_TYPE, + OpCode::TypeQuantity, + dimension.bits(), + &[port], + ) + } + + pub fn declare_constraint( + &mut self, + route: Route, + context: ContextId, + subject: NodeId, + kind: ConstraintKind, + holds: bool, + ) -> Result { + self.ensure_context(context)?; + self.ensure_node(subject)?; + let port = Port::new(subject, PortRole::Subject, 0); + self.insert_committed_node( + route, + context, + META_TYPE, + OpCode::Constraint, + kind.payload(holds), + &[port], + ) + } + + pub fn insert_value( + &mut self, + route: Route, + context: ContextId, + ty: NodeId, + ) -> Result { + self.ensure_context(context)?; + let type_node = self.ensure_type(ty)?; + if type_node.opcode() == OpCode::TypeJudgment { + return Err(Obstruction::EvidenceOnlyType { node: ty }); + } + self.insert_committed_node(route, context, ty, OpCode::Value, 0, &[]) + } + + pub(crate) fn insert_admitted_operation( + &mut self, + routes: [Route; 3], + context: ContextId, + ty: NodeId, + opcode: OpCode, + inputs: &[NodeId], + evidence_plan: EvidencePlan, + ) -> crate::CommitResult { + let [route, judgment_route, evidence_route] = routes; + let before = self.commitment; + + let operation = self.nodes.len() as NodeId; + let operation_first_port = self.ports.len() as u32; + self.ports.extend( + inputs + .iter() + .enumerate() + .map(|(ordinal, target)| Port::new(*target, PortRole::Argument, ordinal as u16)), + ); + self.nodes.push(Node { + payload: 0, + context, + ty, + first_port: operation_first_port, + opcode: opcode as u16, + port_count: inputs.len() as u16, + }); + + let judgment = self.nodes.len() as NodeId; + let judgment_first_port = self.ports.len() as u32; + self.ports.push(Port::new(operation, PortRole::Subject, 0)); + self.ports.extend( + inputs + .iter() + .enumerate() + .map(|(ordinal, target)| Port::new(*target, PortRole::Premise, ordinal as u16 + 1)), + ); + self.ports + .push(Port::new(ty, PortRole::Conclusion, inputs.len() as u16 + 1)); + self.nodes.push(Node { + payload: opcode as u64, + context, + ty: META_TYPE, + first_port: judgment_first_port, + opcode: OpCode::TypeJudgment as u16, + port_count: inputs.len() as u16 + 2, + }); + + let evidence = self.nodes.len() as NodeId; + let evidence_first_port = self.ports.len() as u32; + let (evidence_opcode, evidence_payload, evidence_port_count) = match evidence_plan { + EvidencePlan::Witness => (OpCode::KernelWitness, 0, 0), + EvidencePlan::Obligation { kind, subject } => { + self.ports.push(Port::new(subject, PortRole::Premise, 0)); + (OpCode::Obligation, kind.payload(true), 1) + } + }; + self.nodes.push(Node { + payload: evidence_payload, + context, + ty: judgment, + first_port: evidence_first_port, + opcode: evidence_opcode as u16, + port_count: evidence_port_count, + }); + + self.routes.insert(route, operation); + self.routes.insert(judgment_route, judgment); + self.routes.insert(evidence_route, evidence); + + let after = crate::codec::state_commitment(self); + self.push_event(opcode, operation, before, after); + self.commitment = after; + crate::CommitResult { + node: operation, + evidence, + event: self.journal.len().saturating_sub(1) as EventId, + commitment: after, + } + } +} diff --git a/l64-native/src/graph/context.rs b/l64-native/src/graph/context.rs new file mode 100644 index 0000000..a1821a8 --- /dev/null +++ b/l64-native/src/graph/context.rs @@ -0,0 +1,8 @@ +impl ContextDelta { + fn root() -> Self { + Self { + parent: ROOT_CONTEXT, + binding: NO_NODE, + } + } +} diff --git a/l64-native/src/graph/storage.rs b/l64-native/src/graph/storage.rs new file mode 100644 index 0000000..b6ba2d2 --- /dev/null +++ b/l64-native/src/graph/storage.rs @@ -0,0 +1,127 @@ +impl Graph { + pub(crate) fn validate_context_consistency(&self) -> Result<(), Obstruction> { + for context in 1..self.contexts.len() as ContextId { + let delta = self.ensure_context(context)?; + let binding = delta + .binding() + .ok_or(Obstruction::UnknownContext { context })?; + self.validate_context_binding(delta.parent(), binding)?; + } + Ok(()) + } + + fn validate_context_binding( + &self, + parent: ContextId, + binding: NodeId, + ) -> Result<(), Obstruction> { + let node = self.ensure_node(binding)?; + if node.opcode() != OpCode::Constraint { + return Ok(()); + } + let (subject, kind, holds) = self.constraint_parts(binding)?; + let inherited = self.constraint_state(parent, subject, kind)?; + if matches!( + (inherited, holds), + (ConstraintState::Proven, false) | (ConstraintState::Refuted, true) + ) { + return Err(Obstruction::ContradictoryConstraint { subject, kind }); + } + Ok(()) + } + + pub(crate) fn ensure_node(&self, id: NodeId) -> Result<&Node, Obstruction> { + self.node(id).ok_or(Obstruction::UnknownNode { node: id }) + } + + pub(crate) fn ensure_type(&self, id: NodeId) -> Result<&Node, Obstruction> { + let node = self.ensure_node(id)?; + if !node.opcode().is_type() { + return Err(Obstruction::ExpectedType { node: id }); + } + Ok(node) + } + + pub(crate) fn ensure_context(&self, id: ContextId) -> Result<&ContextDelta, Obstruction> { + self.contexts + .get(id as usize) + .ok_or(Obstruction::UnknownContext { context: id }) + } + + fn insert_committed_node( + &mut self, + route: Route, + context: ContextId, + ty: NodeId, + opcode: OpCode, + payload: u64, + ports: &[Port], + ) -> Result { + if self.routes.contains_key(&route) { + return Err(Obstruction::RouteOccupied); + } + self.ensure_context(context)?; + for port in ports { + self.ensure_node(port.target())?; + } + if ty != META_TYPE { + self.ensure_type(ty)?; + } + + let before = self.commitment; + let node = self.nodes.len() as NodeId; + let first_port = self.ports.len() as u32; + self.ports.extend_from_slice(ports); + self.nodes.push(Node { + payload, + context, + ty, + first_port, + opcode: opcode as u16, + port_count: ports.len() as u16, + }); + self.routes.insert(route, node); + let after = crate::codec::state_commitment(self); + self.push_event(opcode, node, before, after); + self.commitment = after; + Ok(node) + } + + fn push_event( + &mut self, + operation: OpCode, + subject: NodeId, + before: [u8; 32], + after: [u8; 32], + ) { + let parent = self + .journal + .len() + .checked_sub(1) + .map(|value| value as EventId) + .unwrap_or(NO_EVENT); + self.journal.push(JournalEvent { + operation, + subject, + before, + after, + parent, + }); + } + + pub(crate) fn nodes_raw(&self) -> &[Node] { + &self.nodes + } + + pub(crate) fn ports_raw(&self) -> &[Port] { + &self.ports + } + + pub(crate) fn contexts_raw(&self) -> &[ContextDelta] { + &self.contexts + } + + pub(crate) fn routes_raw(&self) -> &BTreeMap { + &self.routes + } +} diff --git a/l64-native/src/graph/typing.rs b/l64-native/src/graph/typing.rs new file mode 100644 index 0000000..3e90405 --- /dev/null +++ b/l64-native/src/graph/typing.rs @@ -0,0 +1,126 @@ +impl Graph { + pub(crate) fn function_parts(&self, ty: NodeId) -> Result<(NodeId, NodeId), Obstruction> { + let node = self.ensure_type(ty)?; + if node.opcode() != OpCode::TypeFunction { + return Err(Obstruction::ExpectedFunctionType { node: ty }); + } + let ports = self + .ports(ty) + .ok_or(Obstruction::UnknownNode { node: ty })?; + if ports.len() != 2 { + return Err(Obstruction::MalformedType { node: ty }); + } + Ok((ports[0].target(), ports[1].target())) + } + + pub(crate) fn matrix_parts(&self, ty: NodeId) -> Result<(NodeId, u32, u32), Obstruction> { + let node = self.ensure_type(ty)?; + if node.opcode() != OpCode::TypeMatrix { + return Err(Obstruction::ExpectedMatrixType { node: ty }); + } + let ports = self + .ports(ty) + .ok_or(Obstruction::UnknownNode { node: ty })?; + if ports.len() != 1 { + return Err(Obstruction::MalformedType { node: ty }); + } + Ok(( + ports[0].target(), + (node.payload >> 32) as u32, + node.payload as u32, + )) + } + + pub(crate) fn matches_function_type( + &self, + ty: NodeId, + domain: NodeId, + codomain: NodeId, + ) -> bool { + self.function_parts(ty) == Ok((domain, codomain)) + } + + pub(crate) fn quantity_parts(&self, ty: NodeId) -> Result<(NodeId, Dimension), Obstruction> { + let node = self.ensure_type(ty)?; + if node.opcode() != OpCode::TypeQuantity { + return Err(Obstruction::ExpectedQuantityType { node: ty }); + } + let ports = self + .ports(ty) + .ok_or(Obstruction::UnknownNode { node: ty })?; + let dimension = + Dimension::from_bits(node.payload()).ok_or(Obstruction::MalformedType { node: ty })?; + if ports.len() != 1 { + return Err(Obstruction::MalformedType { node: ty }); + } + Ok((ports[0].target(), dimension)) + } + + pub(crate) fn matches_matrix_type( + &self, + ty: NodeId, + element: NodeId, + rows: u32, + cols: u32, + ) -> bool { + self.matrix_parts(ty) == Ok((element, rows, cols)) + } + + pub(crate) fn matches_quantity_type( + &self, + ty: NodeId, + carrier: NodeId, + dimension: Dimension, + ) -> bool { + self.quantity_parts(ty) == Ok((carrier, dimension)) + } + + pub(crate) fn constraint_parts( + &self, + node: NodeId, + ) -> Result<(NodeId, ConstraintKind, bool), Obstruction> { + let constraint = self.ensure_node(node)?; + if constraint.opcode() != OpCode::Constraint { + return Err(Obstruction::MalformedConstraint { node }); + } + let ports = self + .ports(node) + .ok_or(Obstruction::MalformedConstraint { node })?; + let (kind, holds) = ConstraintKind::from_payload(constraint.payload()) + .ok_or(Obstruction::MalformedConstraint { node })?; + if ports.len() != 1 || ports[0].role() != PortRole::Subject { + return Err(Obstruction::MalformedConstraint { node }); + } + Ok((ports[0].target(), kind, holds)) + } + + pub(crate) fn constraint_state( + &self, + context: ContextId, + subject: NodeId, + kind: ConstraintKind, + ) -> Result { + let mut cursor = context; + loop { + let delta = self.ensure_context(cursor)?; + if let Some(binding) = delta.binding() + && self + .node(binding) + .is_some_and(|node| node.opcode() == OpCode::Constraint) + { + let (bound_subject, bound_kind, holds) = self.constraint_parts(binding)?; + if bound_subject == subject && bound_kind == kind { + return Ok(if holds { + ConstraintState::Proven + } else { + ConstraintState::Refuted + }); + } + } + if cursor == ROOT_CONTEXT { + return Ok(ConstraintState::Unknown); + } + cursor = delta.parent(); + } + } +} diff --git a/l64-native/src/journal.rs b/l64-native/src/journal.rs new file mode 100644 index 0000000..61a71d6 --- /dev/null +++ b/l64-native/src/journal.rs @@ -0,0 +1,32 @@ +use crate::{EventId, NodeId, OpCode}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct JournalEvent { + pub(crate) operation: OpCode, + pub(crate) subject: NodeId, + pub(crate) before: [u8; 32], + pub(crate) after: [u8; 32], + pub(crate) parent: EventId, +} + +impl JournalEvent { + pub fn operation(&self) -> OpCode { + self.operation + } + + pub fn subject(&self) -> NodeId { + self.subject + } + + pub fn before(&self) -> [u8; 32] { + self.before + } + + pub fn after(&self) -> [u8; 32] { + self.after + } + + pub fn parent(&self) -> Option { + (self.parent != u32::MAX).then_some(self.parent) + } +} diff --git a/l64-native/src/kernel.rs b/l64-native/src/kernel.rs new file mode 100644 index 0000000..1d7d8b9 --- /dev/null +++ b/l64-native/src/kernel.rs @@ -0,0 +1,6 @@ +use crate::{ContextId, Dimension, Graph, LocusWord, NodeId, Route}; + +include!("kernel/types.rs"); +include!("kernel/proposal.rs"); +include!("kernel/transaction.rs"); +include!("kernel/validation.rs"); diff --git a/l64-native/src/kernel/proposal.rs b/l64-native/src/kernel/proposal.rs new file mode 100644 index 0000000..63f820b --- /dev/null +++ b/l64-native/src/kernel/proposal.rs @@ -0,0 +1,197 @@ +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Proposal { + route: Route, + context: ContextId, + opcode: OpCode, + inputs: Box<[NodeId]>, + output_type: NodeId, +} + +impl Proposal { + fn binary( + route: Route, + context: ContextId, + opcode: OpCode, + left: NodeId, + right: NodeId, + output_type: NodeId, + ) -> Self { + Self { + route, + context, + opcode, + inputs: Box::new([left, right]), + output_type, + } + } + + fn unary( + route: Route, + context: ContextId, + opcode: OpCode, + input: NodeId, + output_type: NodeId, + ) -> Self { + Self { + route, + context, + opcode, + inputs: Box::new([input]), + output_type, + } + } + + pub fn compose( + route: Route, + context: ContextId, + first: NodeId, + second: NodeId, + output_type: NodeId, + ) -> Self { + Self::binary(route, context, OpCode::Compose, first, second, output_type) + } + + pub fn matrix_multiply( + route: Route, + context: ContextId, + left: NodeId, + right: NodeId, + output_type: NodeId, + ) -> Self { + Self::binary(route, context, OpCode::MatMul, left, right, output_type) + } + + pub fn add( + route: Route, + context: ContextId, + left: NodeId, + right: NodeId, + output_type: NodeId, + ) -> Self { + Self::binary(route, context, OpCode::Add, left, right, output_type) + } + + pub fn multiply( + route: Route, + context: ContextId, + left: NodeId, + right: NodeId, + output_type: NodeId, + ) -> Self { + Self::binary(route, context, OpCode::Multiply, left, right, output_type) + } + + pub fn divide( + route: Route, + context: ContextId, + left: NodeId, + right: NodeId, + output_type: NodeId, + ) -> Self { + Self::binary(route, context, OpCode::Divide, left, right, output_type) + } + + pub fn square_root( + route: Route, + context: ContextId, + input: NodeId, + output_type: NodeId, + ) -> Self { + Self::unary(route, context, OpCode::Sqrt, input, output_type) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct CommitResult { + pub node: NodeId, + pub evidence: NodeId, + pub event: u32, + pub commitment: [u8; 32], +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Obstruction { + UnknownNode { + node: NodeId, + }, + UnknownContext { + context: ContextId, + }, + RouteOccupied, + ExpectedType { + node: NodeId, + }, + ExpectedFunctionType { + node: NodeId, + }, + ExpectedMatrixType { + node: NodeId, + }, + ExpectedQuantityType { + node: NodeId, + }, + MalformedType { + node: NodeId, + }, + EvidenceOnlyType { + node: NodeId, + }, + Arity { + expected: u16, + actual: u16, + }, + FunctionBoundaryMismatch { + left: NodeId, + right: NodeId, + }, + MatrixShapeMismatch { + left_cols: u32, + right_rows: u32, + }, + MatrixElementMismatch { + left: NodeId, + right: NodeId, + }, + FunctionOutputMismatch { + output_type: NodeId, + domain: NodeId, + codomain: NodeId, + }, + MatrixOutputMismatch { + output_type: NodeId, + element: NodeId, + rows: u32, + cols: u32, + }, + QuantityCarrierMismatch { + left: NodeId, + right: NodeId, + }, + DimensionMismatch { + left: Dimension, + right: Dimension, + }, + DimensionOverflow, + NonIntegralDimensionRoot { + dimension: Dimension, + }, + GuardViolated { + subject: NodeId, + kind: ConstraintKind, + }, + QuantityOutputMismatch { + output_type: NodeId, + carrier: NodeId, + dimension: Dimension, + }, + MalformedConstraint { + node: NodeId, + }, + ContradictoryConstraint { + subject: NodeId, + kind: ConstraintKind, + }, + MalformedEvidence { + node: NodeId, + }, +} diff --git a/l64-native/src/kernel/transaction.rs b/l64-native/src/kernel/transaction.rs new file mode 100644 index 0000000..e759638 --- /dev/null +++ b/l64-native/src/kernel/transaction.rs @@ -0,0 +1,183 @@ +impl Graph { + pub fn transact(&mut self, proposal: Proposal) -> Result { + self.ensure_context(proposal.context)?; + self.ensure_type(proposal.output_type)?; + let expected = proposal + .opcode + .arity() + .expect("public proposal constructors expose executable operations only"); + if proposal.inputs.len() != expected as usize { + return Err(Obstruction::Arity { + expected, + actual: proposal.inputs.len() as u16, + }); + } + + let judgment_route = proposal.route.composed(JUDGMENT_LOCUS); + let evidence_route = proposal.route.composed(EVIDENCE_LOCUS); + if self.resolve(&proposal.route).is_some() + || self.resolve(&judgment_route).is_some() + || self.resolve(&evidence_route).is_some() + { + return Err(Obstruction::RouteOccupied); + } + + let evidence = self.validate_operation( + proposal.context, + proposal.opcode, + &proposal.inputs, + proposal.output_type, + )?; + + Ok(self.insert_admitted_operation( + [proposal.route, judgment_route, evidence_route], + proposal.context, + proposal.output_type, + proposal.opcode, + &proposal.inputs, + evidence, + )) + } + + pub(crate) fn validate_operation( + &self, + _context: ContextId, + opcode: OpCode, + inputs: &[NodeId], + output_type: NodeId, + ) -> Result { + let expected = opcode.arity().ok_or(Obstruction::Arity { + expected: 0, + actual: inputs.len() as u16, + })?; + if inputs.len() != expected as usize { + return Err(Obstruction::Arity { + expected, + actual: inputs.len() as u16, + }); + } + self.ensure_type(output_type)?; + + let left = inputs[0]; + let left_ty = self + .ensure_node(left)? + .ty() + .ok_or(Obstruction::ExpectedType { node: left })?; + let right = inputs.get(1).copied(); + let right_ty = right + .map(|node| { + self.ensure_node(node)? + .ty() + .ok_or(Obstruction::ExpectedType { node }) + }) + .transpose()?; + + match opcode { + OpCode::Compose => { + let right_ty = right_ty.expect("binary arity validated"); + let (first_domain, first_codomain) = self.function_parts(left_ty)?; + let (second_domain, second_codomain) = self.function_parts(right_ty)?; + if first_codomain != second_domain { + return Err(Obstruction::FunctionBoundaryMismatch { + left: first_codomain, + right: second_domain, + }); + } + if !self.matches_function_type(output_type, first_domain, second_codomain) { + return Err(Obstruction::FunctionOutputMismatch { + output_type, + domain: first_domain, + codomain: second_codomain, + }); + } + } + OpCode::MatMul => { + let right_ty = right_ty.expect("binary arity validated"); + let (left_element, left_rows, left_cols) = self.matrix_parts(left_ty)?; + let (right_element, right_rows, right_cols) = self.matrix_parts(right_ty)?; + if left_cols != right_rows { + return Err(Obstruction::MatrixShapeMismatch { + left_cols, + right_rows, + }); + } + if left_element != right_element { + return Err(Obstruction::MatrixElementMismatch { + left: left_element, + right: right_element, + }); + } + if !self.matches_matrix_type(output_type, left_element, left_rows, right_cols) { + return Err(Obstruction::MatrixOutputMismatch { + output_type, + element: left_element, + rows: left_rows, + cols: right_cols, + }); + } + } + OpCode::Add | OpCode::Multiply | OpCode::Divide => { + let right_ty = right_ty.expect("binary arity validated"); + let (left_carrier, left_dimension) = self.quantity_parts(left_ty)?; + let (right_carrier, right_dimension) = self.quantity_parts(right_ty)?; + if left_carrier != right_carrier { + return Err(Obstruction::QuantityCarrierMismatch { + left: left_carrier, + right: right_carrier, + }); + } + let dimension = match opcode { + OpCode::Add => { + if left_dimension != right_dimension { + return Err(Obstruction::DimensionMismatch { + left: left_dimension, + right: right_dimension, + }); + } + left_dimension + } + OpCode::Multiply => left_dimension + .multiply(right_dimension) + .ok_or(Obstruction::DimensionOverflow)?, + OpCode::Divide => left_dimension + .divide(right_dimension) + .ok_or(Obstruction::DimensionOverflow)?, + _ => unreachable!(), + }; + if !self.matches_quantity_type(output_type, left_carrier, dimension) { + return Err(Obstruction::QuantityOutputMismatch { + output_type, + carrier: left_carrier, + dimension, + }); + } + } + OpCode::Sqrt => { + let (carrier, dimension) = self.quantity_parts(left_ty)?; + let output_dimension = dimension + .square_root() + .ok_or(Obstruction::NonIntegralDimensionRoot { dimension })?; + if !self.matches_quantity_type(output_type, carrier, output_dimension) { + return Err(Obstruction::QuantityOutputMismatch { + output_type, + carrier, + dimension: output_dimension, + }); + } + return match self.constraint_state(_context, left, ConstraintKind::NonNegative)? { + ConstraintState::Proven => Ok(EvidencePlan::Witness), + ConstraintState::Refuted => Err(Obstruction::GuardViolated { + subject: left, + kind: ConstraintKind::NonNegative, + }), + ConstraintState::Unknown => Ok(EvidencePlan::Obligation { + kind: ConstraintKind::NonNegative, + subject: left, + }), + }; + } + _ => unreachable!("validated opcode is executable"), + } + Ok(EvidencePlan::Witness) + } +} diff --git a/l64-native/src/kernel/types.rs b/l64-native/src/kernel/types.rs new file mode 100644 index 0000000..28b8f4f --- /dev/null +++ b/l64-native/src/kernel/types.rs @@ -0,0 +1,197 @@ +pub(crate) const JUDGMENT_LOCUS: LocusWord = LocusWord(u64::MAX - 1); +pub(crate) const EVIDENCE_LOCUS: LocusWord = LocusWord(u64::MAX); + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u16)] +pub enum OpCode { + TypeAtom = 1, + TypeMatrix = 2, + TypeFunction = 3, + Value = 4, + Compose = 5, + MatMul = 6, + ExtendContext = 7, + TypeJudgment = 8, + KernelWitness = 9, + TypeQuantity = 10, + Add = 11, + Multiply = 12, + Divide = 13, + Constraint = 14, + Sqrt = 15, + Obligation = 16, +} + +impl OpCode { + pub(crate) fn from_raw(value: u16) -> Option { + match value { + 1 => Some(Self::TypeAtom), + 2 => Some(Self::TypeMatrix), + 3 => Some(Self::TypeFunction), + 4 => Some(Self::Value), + 5 => Some(Self::Compose), + 6 => Some(Self::MatMul), + 7 => Some(Self::ExtendContext), + 8 => Some(Self::TypeJudgment), + 9 => Some(Self::KernelWitness), + 10 => Some(Self::TypeQuantity), + 11 => Some(Self::Add), + 12 => Some(Self::Multiply), + 13 => Some(Self::Divide), + 14 => Some(Self::Constraint), + 15 => Some(Self::Sqrt), + 16 => Some(Self::Obligation), + _ => None, + } + } + + pub(crate) fn is_type(self) -> bool { + matches!( + self, + Self::TypeAtom + | Self::TypeMatrix + | Self::TypeFunction + | Self::TypeJudgment + | Self::TypeQuantity + ) + } + + pub(crate) fn is_untyped_authority(self) -> bool { + self.is_type() || self == Self::Constraint + } + + pub(crate) fn is_persisted_node(self) -> bool { + self != Self::ExtendContext + } + + pub(crate) fn is_executable(self) -> bool { + matches!( + self, + Self::Compose | Self::MatMul | Self::Add | Self::Multiply | Self::Divide | Self::Sqrt + ) + } + + pub(crate) fn arity(self) -> Option { + match self { + Self::Compose | Self::MatMul | Self::Add | Self::Multiply | Self::Divide => Some(2), + Self::Sqrt => Some(1), + _ => None, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub enum ConstraintKind { + NonNegative = 1, +} + +impl ConstraintKind { + pub(crate) fn from_raw(value: u8) -> Option { + match value { + 1 => Some(Self::NonNegative), + _ => None, + } + } + + pub(crate) fn payload(self, holds: bool) -> u64 { + self as u64 | (u64::from(holds) << 8) + } + + pub(crate) fn from_payload(payload: u64) -> Option<(Self, bool)> { + if payload >> 9 != 0 { + return None; + } + let kind = Self::from_raw(payload as u8)?; + let truth = ((payload >> 8) & 1) != 0; + Some((kind, truth)) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum ConstraintState { + Proven, + Refuted, + Unknown, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum EvidencePlan { + Witness, + Obligation { + kind: ConstraintKind, + subject: NodeId, + }, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub enum PortRole { + Parameter = 1, + Domain = 2, + Codomain = 3, + Argument = 4, + Subject = 5, + Premise = 6, + Conclusion = 7, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(C)] +pub struct Port { + target: NodeId, + role: u8, + flags: u8, + ordinal: u16, +} + +impl PortRole { + pub(crate) fn from_raw(value: u8) -> Option { + match value { + 1 => Some(Self::Parameter), + 2 => Some(Self::Domain), + 3 => Some(Self::Codomain), + 4 => Some(Self::Argument), + 5 => Some(Self::Subject), + 6 => Some(Self::Premise), + 7 => Some(Self::Conclusion), + _ => None, + } + } +} + +impl Port { + pub(crate) fn new(target: NodeId, role: PortRole, ordinal: u16) -> Self { + Self { + target, + role: role as u8, + flags: 0, + ordinal, + } + } + + pub(crate) fn from_raw(target: NodeId, role: PortRole, flags: u8, ordinal: u16) -> Self { + Self { + target, + role: role as u8, + flags, + ordinal, + } + } + + pub fn target(&self) -> NodeId { + self.target + } + + pub fn role(&self) -> PortRole { + PortRole::from_raw(self.role).expect("stored port role is validated at insertion") + } + + pub fn ordinal(&self) -> u16 { + self.ordinal + } + + pub(crate) fn flags(&self) -> u8 { + self.flags + } +} diff --git a/l64-native/src/kernel/validation.rs b/l64-native/src/kernel/validation.rs new file mode 100644 index 0000000..aa1d3e9 --- /dev/null +++ b/l64-native/src/kernel/validation.rs @@ -0,0 +1,84 @@ +impl Graph { + pub(crate) fn validate_decoded_authority(&self) -> Result<(), Obstruction> { + self.validate_context_consistency()?; + let mut attached = vec![false; self.node_count()]; + for (route, node_id) in self.routes_raw() { + let node = self + .node(*node_id) + .ok_or(Obstruction::UnknownNode { node: *node_id })?; + if !node.opcode().is_executable() { + continue; + } + let operation_ports = self + .ports(*node_id) + .ok_or(Obstruction::MalformedEvidence { node: *node_id })?; + let inputs = operation_ports.iter().map(Port::target).collect::>(); + let output_type = node + .ty() + .ok_or(Obstruction::ExpectedType { node: *node_id })?; + let plan = + self.validate_operation(node.context(), node.opcode(), &inputs, output_type)?; + + let judgment = self + .resolve(&route.composed(JUDGMENT_LOCUS)) + .ok_or(Obstruction::MalformedEvidence { node: *node_id })?; + let evidence = self + .resolve(&route.composed(EVIDENCE_LOCUS)) + .ok_or(Obstruction::MalformedEvidence { node: *node_id })?; + let judgment_node = self + .node(judgment) + .ok_or(Obstruction::MalformedEvidence { node: judgment })?; + let judgment_ports = self + .ports(judgment) + .ok_or(Obstruction::MalformedEvidence { node: judgment })?; + if judgment_node.opcode() != OpCode::TypeJudgment + || judgment_node.payload() != node.opcode() as u64 + || judgment_ports.len() != inputs.len() + 2 + || judgment_ports[0].target() != *node_id + || judgment_ports.last().map(Port::target) != Some(output_type) + || judgment_ports[1..judgment_ports.len() - 1] + .iter() + .map(Port::target) + .ne(inputs.iter().copied()) + { + return Err(Obstruction::MalformedEvidence { node: judgment }); + } + let evidence_node = self + .node(evidence) + .ok_or(Obstruction::MalformedEvidence { node: evidence })?; + let evidence_ports = self + .ports(evidence) + .ok_or(Obstruction::MalformedEvidence { node: evidence })?; + let evidence_valid = match plan { + EvidencePlan::Witness => { + evidence_node.opcode() == OpCode::KernelWitness && evidence_ports.is_empty() + } + EvidencePlan::Obligation { kind, subject } => { + evidence_node.opcode() == OpCode::Obligation + && ConstraintKind::from_payload(evidence_node.payload()) + == Some((kind, true)) + && evidence_ports.len() == 1 + && evidence_ports[0].role() == PortRole::Premise + && evidence_ports[0].target() == subject + } + }; + if !evidence_valid || evidence_node.ty() != Some(judgment) { + return Err(Obstruction::MalformedEvidence { node: evidence }); + } + attached[judgment as usize] = true; + attached[evidence as usize] = true; + } + for (index, node) in self.nodes_raw().iter().enumerate() { + if matches!( + node.opcode(), + OpCode::TypeJudgment | OpCode::KernelWitness | OpCode::Obligation + ) && !attached[index] + { + return Err(Obstruction::MalformedEvidence { + node: index as NodeId, + }); + } + } + Ok(()) + } +} diff --git a/l64-native/src/lib.rs b/l64-native/src/lib.rs new file mode 100644 index 0000000..8e31bcc --- /dev/null +++ b/l64-native/src/lib.rs @@ -0,0 +1,23 @@ +#![forbid(unsafe_code)] + +mod codec; +mod context; +mod dimension; +mod dna; +mod graph; +mod journal; +mod kernel; +mod rna; +mod route; + +pub use codec::{DecodeError, canonical_bytes, decode_canonical}; +pub use context::ContextDelta; +pub use dimension::Dimension; +pub use dna::{DnaError, MAX_NATIVE_DNA_PAYLOAD_BYTES, decode_dna, dna_bytes}; +pub use graph::{ContextId, EventId, Graph, Node, NodeId, ROOT_CONTEXT}; +pub use journal::JournalEvent; +pub use kernel::{CommitResult, ConstraintKind, Obstruction, OpCode, Port, PortRole, Proposal}; +pub use rna::{ + MAX_NATIVE_RNA_BYTES, RnaError, compile_rna, dna_to_rna, normalize_rna, rna_bytes, rna_to_dna, +}; +pub use route::{LocusWord, Route}; diff --git a/l64-native/src/rna.rs b/l64-native/src/rna.rs new file mode 100644 index 0000000..294c7b3 --- /dev/null +++ b/l64-native/src/rna.rs @@ -0,0 +1,42 @@ +use std::collections::BTreeMap; + +use crate::{ + ConstraintKind, ContextId, Dimension, DnaError, Graph, LocusWord, NodeId, Obstruction, OpCode, + Proposal, ROOT_CONTEXT, Route, decode_dna, dna_bytes, +}; + +const RNA_MAGIC: &[u8] = b"L64R1"; +pub const MAX_NATIVE_RNA_BYTES: usize = 1 << 24; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RnaError { + SourceTooLarge, + Empty, + BadHeader, + InvalidArity { line: u32 }, + InvalidNumber { line: u32 }, + UnknownInstruction { line: u32 }, + SlotOrder { line: u32, slot: u64 }, + ContextOrder { line: u32, context: u32 }, + UnknownSlot { line: u32, slot: u64 }, + UnrepresentableGraph, + Graph(Obstruction), + Dna(DnaError), +} + +impl From for RnaError { + fn from(value: Obstruction) -> Self { + Self::Graph(value) + } +} + +impl From for RnaError { + fn from(value: DnaError) -> Self { + Self::Dna(value) + } +} + +include!("rna/compile.rs"); +include!("rna/sequence.rs"); +include!("rna/parse.rs"); +include!("rna/render.rs"); diff --git a/l64-native/src/rna/compile.rs b/l64-native/src/rna/compile.rs new file mode 100644 index 0000000..5e13694 --- /dev/null +++ b/l64-native/src/rna/compile.rs @@ -0,0 +1,134 @@ +pub fn compile_rna(source: &[u8]) -> Result { + if source.len() > MAX_NATIVE_RNA_BYTES { + return Err(RnaError::SourceTooLarge); + } + + let mut lines = source.split(|byte| *byte == b'\n').enumerate(); + let (header_line, header) = next_nonempty(&mut lines).ok_or(RnaError::Empty)?; + let header_tokens = tokens(header); + if header_tokens.len() != 2 || header_tokens[0] != RNA_MAGIC { + return Err(RnaError::BadHeader); + } + let domain = parse_u64(header_tokens[1]).ok_or(RnaError::InvalidNumber { + line: line_number(header_line), + })?; + + let root = Route::root(LocusWord(domain)); + let mut graph = Graph::new(); + let mut slots = BTreeMap::new(); + let mut last_slot = None; + let mut instruction_count = 0usize; + + for (line_index, raw_line) in lines { + let source_line = trim_ascii(raw_line); + if source_line.is_empty() { + continue; + } + let parts = tokens(source_line); + let line = line_number(line_index); + let instruction = parts + .first() + .copied() + .ok_or(RnaError::InvalidArity { line })?; + + if instruction == b"h" { + if parts.len() != 4 { + return Err(RnaError::InvalidArity { line }); + } + let context = parse_u32_part(&parts, 1, line)?; + if context as usize != graph.context_count() { + return Err(RnaError::ContextOrder { line, context }); + } + let parent = parse_u32_part(&parts, 2, line)?; + let binding = resolve_part(&slots, &parts, 3, line)?; + let created = graph.extend_context(parent, binding)?; + if created != context { + return Err(RnaError::ContextOrder { line, context }); + } + continue; + } + + let slot = parse_part(&parts, 1, line)?; + if let Some(previous) = last_slot + && slot <= previous + { + return Err(RnaError::SlotOrder { line, slot }); + } + let route = root.composed(LocusWord(slot)); + + let node = match instruction { + b"a" if parts.len() == 3 => { + graph.declare_atom_type(route, LocusWord(parse_part(&parts, 2, line)?))? + } + b"m" if parts.len() == 5 => { + let element = resolve_part(&slots, &parts, 2, line)?; + let rows = parse_u32_part(&parts, 3, line)?; + let cols = parse_u32_part(&parts, 4, line)?; + graph.declare_matrix_type(route, element, rows, cols)? + } + b"f" if parts.len() == 4 => { + let domain = resolve_part(&slots, &parts, 2, line)?; + let codomain = resolve_part(&slots, &parts, 3, line)?; + graph.declare_function_type(route, domain, codomain)? + } + b"q" if parts.len() == 10 => { + let carrier = resolve_part(&slots, &parts, 2, line)?; + let mut exponents = [0_i8; 7]; + for (axis, exponent) in exponents.iter_mut().enumerate() { + *exponent = parse_i8_part(&parts, axis + 3, line)?; + } + graph.declare_quantity_type(route, carrier, Dimension::new(exponents))? + } + b"v" if matches!(parts.len(), 3 | 4) => { + let ty = resolve_part(&slots, &parts, 2, line)?; + let context = parse_optional_context(&parts, 3, line)?; + graph.insert_value(route, context, ty)? + } + b"k" if matches!(parts.len(), 5 | 6) => { + let subject = resolve_part(&slots, &parts, 2, line)?; + let kind_raw = parse_u8_part(&parts, 3, line)?; + let kind = + ConstraintKind::from_raw(kind_raw).ok_or(RnaError::InvalidNumber { line })?; + let holds = parse_bool_part(&parts, 4, line)?; + let context = parse_optional_context(&parts, 5, line)?; + graph.declare_constraint(route, context, subject, kind, holds)? + } + b"c" | b"x" | b"+" | b"*" | b"/" if matches!(parts.len(), 5 | 6) => { + let left = resolve_part(&slots, &parts, 2, line)?; + let right = resolve_part(&slots, &parts, 3, line)?; + let output = resolve_part(&slots, &parts, 4, line)?; + let context = parse_optional_context(&parts, 5, line)?; + let proposal = match instruction { + b"c" => Proposal::compose(route, context, left, right, output), + b"x" => Proposal::matrix_multiply(route, context, left, right, output), + b"+" => Proposal::add(route, context, left, right, output), + b"*" => Proposal::multiply(route, context, left, right, output), + b"/" => Proposal::divide(route, context, left, right, output), + _ => unreachable!(), + }; + graph.transact(proposal)?.node + } + b"r" if matches!(parts.len(), 4 | 5) => { + let input = resolve_part(&slots, &parts, 2, line)?; + let output = resolve_part(&slots, &parts, 3, line)?; + let context = parse_optional_context(&parts, 4, line)?; + graph + .transact(Proposal::square_root(route, context, input, output))? + .node + } + b"a" | b"m" | b"f" | b"q" | b"v" | b"k" | b"c" | b"x" | b"+" | b"*" | b"/" | b"r" => { + return Err(RnaError::InvalidArity { line }); + } + _ => return Err(RnaError::UnknownInstruction { line }), + }; + + slots.insert(slot, node); + last_slot = Some(slot); + instruction_count += 1; + } + + if instruction_count == 0 { + return Err(RnaError::Empty); + } + Ok(graph) +} diff --git a/l64-native/src/rna/parse.rs b/l64-native/src/rna/parse.rs new file mode 100644 index 0000000..61832ae --- /dev/null +++ b/l64-native/src/rna/parse.rs @@ -0,0 +1,131 @@ +fn require_root(context: ContextId) -> Result<(), RnaError> { + (context == ROOT_CONTEXT) + .then_some(()) + .ok_or(RnaError::UnrepresentableGraph) +} + +fn push_context(out: &mut Vec, context: ContextId) { + if context != ROOT_CONTEXT { + push_space_decimal(out, u64::from(context)); + } +} + +fn next_nonempty<'a>( + lines: &mut impl Iterator, +) -> Option<(usize, &'a [u8])> { + lines.find_map(|(index, line)| { + let line = trim_ascii(line); + (!line.is_empty()).then_some((index, line)) + }) +} + +fn trim_ascii(mut bytes: &[u8]) -> &[u8] { + while bytes.first().is_some_and(u8::is_ascii_whitespace) { + bytes = &bytes[1..]; + } + while bytes.last().is_some_and(u8::is_ascii_whitespace) { + bytes = &bytes[..bytes.len() - 1]; + } + bytes +} + +fn tokens(line: &[u8]) -> Vec<&[u8]> { + line.split(|byte| byte.is_ascii_whitespace()) + .filter(|part| !part.is_empty()) + .collect() +} + +fn parse_part(parts: &[&[u8]], index: usize, line: u32) -> Result { + parts + .get(index) + .and_then(|part| parse_u64(part)) + .ok_or(RnaError::InvalidNumber { line }) +} + +fn parse_u8_part(parts: &[&[u8]], index: usize, line: u32) -> Result { + parse_part(parts, index, line)? + .try_into() + .map_err(|_| RnaError::InvalidNumber { line }) +} + +fn parse_u32_part(parts: &[&[u8]], index: usize, line: u32) -> Result { + parse_part(parts, index, line)? + .try_into() + .map_err(|_| RnaError::InvalidNumber { line }) +} + +fn parse_i8_part(parts: &[&[u8]], index: usize, line: u32) -> Result { + parts + .get(index) + .and_then(|part| parse_i8(part)) + .ok_or(RnaError::InvalidNumber { line }) +} + +fn parse_bool_part(parts: &[&[u8]], index: usize, line: u32) -> Result { + match parse_u8_part(parts, index, line)? { + 0 => Ok(false), + 1 => Ok(true), + _ => Err(RnaError::InvalidNumber { line }), + } +} + +fn parse_optional_context(parts: &[&[u8]], index: usize, line: u32) -> Result { + if parts.len() == index { + Ok(ROOT_CONTEXT) + } else { + parse_u32_part(parts, index, line) + } +} + +fn resolve_part( + slots: &BTreeMap, + parts: &[&[u8]], + index: usize, + line: u32, +) -> Result { + let slot = parse_part(parts, index, line)?; + slots + .get(&slot) + .copied() + .ok_or(RnaError::UnknownSlot { line, slot }) +} + +fn parse_i8(bytes: &[u8]) -> Option { + let (negative, digits) = if let Some(digits) = bytes.strip_prefix(b"-") { + (true, digits) + } else if let Some(digits) = bytes.strip_prefix(b"+") { + (false, digits) + } else { + (false, bytes) + }; + let magnitude = parse_u64(digits)?; + if negative { + let magnitude: i16 = magnitude.try_into().ok()?; + i8::try_from(-magnitude).ok() + } else { + magnitude.try_into().ok() + } +} + +fn parse_u64(bytes: &[u8]) -> Option { + let (radix, digits) = if bytes.starts_with(b"0x") || bytes.starts_with(b"0X") { + (16u64, &bytes[2..]) + } else { + (10u64, bytes) + }; + if digits.is_empty() { + return None; + } + digits.iter().try_fold(0u64, |value, byte| { + let digit = match *byte { + b'0'..=b'9' => u64::from(*byte - b'0'), + b'a'..=b'f' if radix == 16 => u64::from(*byte - b'a' + 10), + b'A'..=b'F' if radix == 16 => u64::from(*byte - b'A' + 10), + _ => return None, + }; + if digit >= radix { + return None; + } + value.checked_mul(radix)?.checked_add(digit) + }) +} diff --git a/l64-native/src/rna/render.rs b/l64-native/src/rna/render.rs new file mode 100644 index 0000000..4456df7 --- /dev/null +++ b/l64-native/src/rna/render.rs @@ -0,0 +1,70 @@ +fn slot_for(slots: &BTreeMap, node: NodeId) -> Result { + slots + .get(&node) + .copied() + .ok_or(RnaError::UnrepresentableGraph) +} + +fn push_prefix(out: &mut Vec, opcode: u8, slot: u64) { + out.push(opcode); + push_space_decimal(out, slot); +} + +fn push_space_decimal(out: &mut Vec, value: u64) { + out.push(b' '); + push_decimal(out, value); +} + +fn push_space_signed(out: &mut Vec, value: i8) { + out.push(b' '); + if value < 0 { + out.push(b'-'); + push_decimal(out, u64::from(value.unsigned_abs())); + } else { + push_decimal(out, value as u64); + } +} + +fn push_space_hex(out: &mut Vec, value: u64) { + out.push(b' '); + push_hex(out, value); +} + +fn push_decimal(out: &mut Vec, mut value: u64) { + let mut buffer = [0u8; 20]; + let mut cursor = buffer.len(); + loop { + cursor -= 1; + buffer[cursor] = b'0' + (value % 10) as u8; + value /= 10; + if value == 0 { + break; + } + } + out.extend_from_slice(&buffer[cursor..]); +} + +fn push_hex(out: &mut Vec, mut value: u64) { + out.extend_from_slice(b"0x"); + if value == 0 { + out.push(b'0'); + return; + } + let mut buffer = [0u8; 16]; + let mut cursor = buffer.len(); + while value != 0 { + cursor -= 1; + let digit = (value & 0xf) as u8; + buffer[cursor] = if digit < 10 { + b'0' + digit + } else { + b'a' + digit - 10 + }; + value >>= 4; + } + out.extend_from_slice(&buffer[cursor..]); +} + +fn line_number(index: usize) -> u32 { + u32::try_from(index + 1).unwrap_or(u32::MAX) +} diff --git a/l64-native/src/rna/sequence.rs b/l64-native/src/rna/sequence.rs new file mode 100644 index 0000000..6c2e663 --- /dev/null +++ b/l64-native/src/rna/sequence.rs @@ -0,0 +1,187 @@ +pub fn normalize_rna(source: &[u8]) -> Result, RnaError> { + rna_bytes(&compile_rna(source)?) +} + +pub fn rna_to_dna(source: &[u8]) -> Result, RnaError> { + Ok(dna_bytes(&compile_rna(source)?)?) +} + +pub fn dna_to_rna(source: &[u8]) -> Result, RnaError> { + rna_bytes(&decode_dna(source)?) +} + +pub fn rna_bytes(graph: &Graph) -> Result, RnaError> { + let mut domain = None; + let mut authored = BTreeMap::new(); + for (route, node) in graph.routes_raw() { + if route.tail().len() != 1 { + continue; + } + match domain { + Some(existing) if existing != route.domain() => { + return Err(RnaError::UnrepresentableGraph); + } + None => domain = Some(route.domain()), + _ => {} + } + if authored.insert(*node, route.tail()[0].0).is_some() { + return Err(RnaError::UnrepresentableGraph); + } + } + + let domain = domain.ok_or(RnaError::Empty)?; + let expected_authored = graph + .nodes_raw() + .iter() + .filter(|node| { + !matches!( + node.opcode(), + OpCode::TypeJudgment | OpCode::KernelWitness | OpCode::Obligation + ) + }) + .count(); + if authored.len() != expected_authored { + return Err(RnaError::UnrepresentableGraph); + } + + let mut slots = BTreeMap::new(); + let mut previous_slot = None; + for (node, slot) in &authored { + if previous_slot.is_some_and(|previous| *slot <= previous) { + return Err(RnaError::UnrepresentableGraph); + } + slots.insert(*node, *slot); + previous_slot = Some(*slot); + } + + let mut out = Vec::with_capacity(32 + authored.len() * 32 + graph.context_count() * 16); + out.extend_from_slice(RNA_MAGIC); + out.push(b' '); + push_hex(&mut out, domain.0); + out.push(b'\n'); + + let mut next_context = 1_u32; + for (node_id, slot) in authored { + emit_ready_contexts(graph, &slots, &mut out, &mut next_context, node_id)?; + emit_node(graph, &slots, &mut out, node_id, slot)?; + } + emit_ready_contexts(graph, &slots, &mut out, &mut next_context, u32::MAX)?; + if next_context as usize != graph.context_count() { + return Err(RnaError::UnrepresentableGraph); + } + + Ok(out) +} + +fn emit_ready_contexts( + graph: &Graph, + slots: &BTreeMap, + out: &mut Vec, + next_context: &mut ContextId, + before_node: NodeId, +) -> Result<(), RnaError> { + while (*next_context as usize) < graph.context_count() { + let context = &graph.contexts_raw()[*next_context as usize]; + let binding = context.binding().ok_or(RnaError::UnrepresentableGraph)?; + if binding >= before_node { + break; + } + out.push(b'h'); + push_space_decimal(out, u64::from(*next_context)); + push_space_decimal(out, u64::from(context.parent())); + push_space_decimal(out, slot_for(slots, binding)?); + out.push(b'\n'); + *next_context += 1; + } + Ok(()) +} + +fn emit_node( + graph: &Graph, + slots: &BTreeMap, + out: &mut Vec, + node_id: NodeId, + slot: u64, +) -> Result<(), RnaError> { + let node = graph.node(node_id).ok_or(RnaError::UnrepresentableGraph)?; + let ports = graph.ports(node_id).ok_or(RnaError::UnrepresentableGraph)?; + match node.opcode() { + OpCode::TypeAtom if ports.is_empty() && node.ty().is_none() => { + require_root(node.context())?; + push_prefix(out, b'a', slot); + push_space_hex(out, node.payload()); + } + OpCode::TypeMatrix if ports.len() == 1 && node.ty().is_none() => { + require_root(node.context())?; + push_prefix(out, b'm', slot); + push_space_decimal(out, slot_for(slots, ports[0].target())?); + push_space_decimal(out, node.payload() >> 32); + push_space_decimal(out, node.payload() as u32 as u64); + } + OpCode::TypeFunction if ports.len() == 2 && node.ty().is_none() => { + require_root(node.context())?; + push_prefix(out, b'f', slot); + push_space_decimal(out, slot_for(slots, ports[0].target())?); + push_space_decimal(out, slot_for(slots, ports[1].target())?); + } + OpCode::TypeQuantity if ports.len() == 1 && node.ty().is_none() => { + require_root(node.context())?; + push_prefix(out, b'q', slot); + push_space_decimal(out, slot_for(slots, ports[0].target())?); + let dimension = + Dimension::from_bits(node.payload()).ok_or(RnaError::UnrepresentableGraph)?; + for exponent in dimension.exponents() { + push_space_signed(out, exponent); + } + } + OpCode::Value if ports.is_empty() => { + push_prefix(out, b'v', slot); + push_space_decimal( + out, + slot_for(slots, node.ty().ok_or(RnaError::UnrepresentableGraph)?)?, + ); + push_context(out, node.context()); + } + OpCode::Constraint if ports.len() == 1 => { + push_prefix(out, b'k', slot); + push_space_decimal(out, slot_for(slots, ports[0].target())?); + let (kind, holds) = ConstraintKind::from_payload(node.payload()) + .ok_or(RnaError::UnrepresentableGraph)?; + push_space_decimal(out, kind as u64); + push_space_decimal(out, u64::from(holds)); + push_context(out, node.context()); + } + OpCode::Compose | OpCode::MatMul | OpCode::Add | OpCode::Multiply | OpCode::Divide + if ports.len() == 2 => + { + let opcode = match node.opcode() { + OpCode::Compose => b'c', + OpCode::MatMul => b'x', + OpCode::Add => b'+', + OpCode::Multiply => b'*', + OpCode::Divide => b'/', + _ => unreachable!(), + }; + push_prefix(out, opcode, slot); + push_space_decimal(out, slot_for(slots, ports[0].target())?); + push_space_decimal(out, slot_for(slots, ports[1].target())?); + push_space_decimal( + out, + slot_for(slots, node.ty().ok_or(RnaError::UnrepresentableGraph)?)?, + ); + push_context(out, node.context()); + } + OpCode::Sqrt if ports.len() == 1 => { + push_prefix(out, b'r', slot); + push_space_decimal(out, slot_for(slots, ports[0].target())?); + push_space_decimal( + out, + slot_for(slots, node.ty().ok_or(RnaError::UnrepresentableGraph)?)?, + ); + push_context(out, node.context()); + } + _ => return Err(RnaError::UnrepresentableGraph), + } + out.push(b'\n'); + Ok(()) +} diff --git a/l64-native/src/route.rs b/l64-native/src/route.rs new file mode 100644 index 0000000..c8831e9 --- /dev/null +++ b/l64-native/src/route.rs @@ -0,0 +1,59 @@ +use core::cmp::Ordering; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[repr(transparent)] +pub struct LocusWord(pub u64); + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Route { + domain: LocusWord, + tail: Box<[LocusWord]>, +} + +impl Route { + pub fn root(domain: LocusWord) -> Self { + Self { + domain, + tail: Box::new([]), + } + } + + pub fn from_parts(domain: LocusWord, tail: impl Into>) -> Self { + Self { + domain, + tail: tail.into(), + } + } + + pub fn domain(&self) -> LocusWord { + self.domain + } + + pub fn tail(&self) -> &[LocusWord] { + &self.tail + } + + pub fn composed(&self, next: LocusWord) -> Self { + let mut tail = Vec::with_capacity(self.tail.len() + 1); + tail.extend_from_slice(&self.tail); + tail.push(next); + Self { + domain: self.domain, + tail: tail.into_boxed_slice(), + } + } +} + +impl PartialOrd for Route { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for Route { + fn cmp(&self, other: &Self) -> Ordering { + self.domain + .cmp(&other.domain) + .then_with(|| self.tail.as_ref().cmp(other.tail.as_ref())) + } +} diff --git a/l64-native/tests/architecture.rs b/l64-native/tests/architecture.rs new file mode 100644 index 0000000..6b8cc5d --- /dev/null +++ b/l64-native/tests/architecture.rs @@ -0,0 +1,61 @@ +use core::mem::size_of; +use std::{fs, path::Path}; + +use l64_native::{Dimension, Node, Port}; + +#[test] +fn compact_layout_budgets_hold() { + assert!( + size_of::() <= 32, + "Node grew to {} bytes", + size_of::() + ); + assert!( + size_of::() <= 8, + "Port grew to {} bytes", + size_of::() + ); + assert!( + size_of::() <= 8, + "Dimension grew to {} bytes", + size_of::() + ); +} + +#[test] +fn native_source_rejects_coordination_heavy_dependencies() { + let source_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("src"); + let mut source = std::string::String::new(); + for entry in fs::read_dir(source_root).unwrap() { + let path = entry.unwrap().path(); + if path.extension().and_then(|part| part.to_str()) == Some("rs") { + source.push_str(&fs::read_to_string(path).unwrap()); + } + } + + let forbidden = [ + ["Str", "ing"].concat(), + ["HashMap", "<", "Str", "ing"].concat(), + ["serde", "_json"].concat(), + ["bin", "code"].concat(), + ["Ser", "ialize"].concat(), + ["Deser", "ialize"].concat(), + ["claim", "_packet"].concat(), + ["theorem", "_id"].concat(), + ["campaign", "_id"].concat(), + ]; + for token in forbidden { + assert!(!source.contains(&token), "forbidden native token: {token}"); + } + + let public_structs = source.matches("pub struct ").count(); + let public_enums = source.matches("pub enum ").count(); + assert!( + public_structs <= 12, + "public struct budget exceeded: {public_structs}" + ); + assert!( + public_enums <= 8, + "public enum budget exceeded: {public_enums}" + ); +} diff --git a/l64-native/tests/codec.rs b/l64-native/tests/codec.rs new file mode 100644 index 0000000..32336e9 --- /dev/null +++ b/l64-native/tests/codec.rs @@ -0,0 +1,97 @@ +use l64_native::{ + DecodeError, Graph, LocusWord, Proposal, ROOT_CONTEXT, Route, canonical_bytes, decode_canonical, +}; + +fn route(index: u64) -> Route { + Route::root(LocusWord(0x4c36344e41544956)).composed(LocusWord(index)) +} + +fn representative_graph() -> Graph { + let mut graph = Graph::new(); + let scalar = graph.declare_atom_type(route(1), LocusWord(0x52)).unwrap(); + let a = graph.declare_atom_type(route(2), LocusWord(0x41)).unwrap(); + let b = graph.declare_atom_type(route(3), LocusWord(0x42)).unwrap(); + let c = graph.declare_atom_type(route(4), LocusWord(0x43)).unwrap(); + let ab = graph.declare_function_type(route(5), a, b).unwrap(); + let bc = graph.declare_function_type(route(6), b, c).unwrap(); + let ac = graph.declare_function_type(route(7), a, c).unwrap(); + let first = graph.insert_value(route(8), ROOT_CONTEXT, ab).unwrap(); + let second = graph.insert_value(route(9), ROOT_CONTEXT, bc).unwrap(); + graph + .transact(Proposal::compose( + route(10), + ROOT_CONTEXT, + first, + second, + ac, + )) + .unwrap(); + let matrix = graph.declare_matrix_type(route(11), scalar, 2, 3).unwrap(); + let value = graph.insert_value(route(12), ROOT_CONTEXT, matrix).unwrap(); + graph.extend_context(ROOT_CONTEXT, value).unwrap(); + graph +} + +#[test] +fn canonical_decode_reencode_is_exact_fixed_point() { + let graph = representative_graph(); + let bytes = canonical_bytes(&graph); + let decoded = decode_canonical(&bytes).unwrap(); + + assert_eq!(canonical_bytes(&decoded), bytes); + assert_eq!(decoded.state_commitment(), graph.state_commitment()); + assert_eq!(decoded.node_count(), graph.node_count()); + assert_eq!(decoded.port_count(), graph.port_count()); + assert_eq!(decoded.context_count(), graph.context_count()); + assert_eq!(decoded.journal_len(), 0); + for index in 1..=12 { + assert_eq!(decoded.resolve(&route(index)), graph.resolve(&route(index))); + } +} + +#[test] +fn decoder_rejects_truncation_and_trailing_bytes() { + let bytes = canonical_bytes(&representative_graph()); + assert_eq!( + decode_canonical(&bytes[..bytes.len() - 1]).unwrap_err(), + DecodeError::Truncated + ); + + let mut trailing = bytes; + trailing.push(0); + assert_eq!( + decode_canonical(&trailing).unwrap_err(), + DecodeError::TrailingBytes + ); +} + +#[test] +fn decoder_rejects_unknown_node_opcode() { + let mut bytes = canonical_bytes(&representative_graph()); + let first_node_opcode = 22; + bytes[first_node_opcode..first_node_opcode + 2].copy_from_slice(&u16::MAX.to_le_bytes()); + assert_eq!( + decode_canonical(&bytes).unwrap_err(), + DecodeError::UnknownOpcode { opcode: u16::MAX } + ); +} + +#[test] +fn decoder_rejects_noncanonical_route_order() { + let graph = representative_graph(); + let mut bytes = canonical_bytes(&graph); + let routes_offset = + 22 + graph.node_count() * 24 + graph.port_count() * 8 + graph.context_count() * 8; + let first_route_len = 8 + 4 + 8 + 4; + let first = bytes[routes_offset..routes_offset + first_route_len].to_vec(); + let second = + bytes[routes_offset + first_route_len..routes_offset + 2 * first_route_len].to_vec(); + bytes[routes_offset..routes_offset + first_route_len].copy_from_slice(&second); + bytes[routes_offset + first_route_len..routes_offset + 2 * first_route_len] + .copy_from_slice(&first); + + assert_eq!( + decode_canonical(&bytes).unwrap_err(), + DecodeError::NonCanonical + ); +} diff --git a/l64-native/tests/constraints.rs b/l64-native/tests/constraints.rs new file mode 100644 index 0000000..65a0d4a --- /dev/null +++ b/l64-native/tests/constraints.rs @@ -0,0 +1,328 @@ +use l64_native::{ + ConstraintKind, DecodeError, Dimension, Graph, LocusWord, Obstruction, OpCode, Proposal, + ROOT_CONTEXT, Route, canonical_bytes, decode_canonical, +}; + +fn route(index: u64) -> Route { + Route::root(LocusWord(0x434f4e5354524149)).composed(LocusWord(index)) +} + +fn quantity_fixture() -> (Graph, u32, u32, u32, u32, u32) { + let mut graph = Graph::new(); + let real = graph.declare_atom_type(route(1), LocusWord(0x52)).unwrap(); + let length = graph + .declare_quantity_type(route(2), real, Dimension::LENGTH) + .unwrap(); + let time = graph + .declare_quantity_type(route(3), real, Dimension::TIME) + .unwrap(); + let speed = graph + .declare_quantity_type( + route(4), + real, + Dimension::LENGTH.divide(Dimension::TIME).unwrap(), + ) + .unwrap(); + let area = graph + .declare_quantity_type( + route(5), + real, + Dimension::LENGTH.multiply(Dimension::LENGTH).unwrap(), + ) + .unwrap(); + (graph, real, length, time, speed, area) +} + +#[test] +fn dimension_vector_composes_without_schema_objects() { + let velocity = Dimension::LENGTH.divide(Dimension::TIME).unwrap(); + let acceleration = velocity.divide(Dimension::TIME).unwrap(); + assert_eq!(velocity.exponents(), [1, 0, -1, 0, 0, 0, 0]); + assert_eq!(acceleration.exponents(), [1, 0, -2, 0, 0, 0, 0]); + assert_eq!(Dimension::LENGTH.square_root(), None); + assert_eq!( + Dimension::LENGTH + .multiply(Dimension::LENGTH) + .unwrap() + .square_root(), + Some(Dimension::LENGTH) + ); +} + +#[test] +fn incompatible_addition_rejects_without_mutation() { + let (mut graph, _, length, time, _, _) = quantity_fixture(); + let left = graph.insert_value(route(10), ROOT_CONTEXT, length).unwrap(); + let right = graph.insert_value(route(11), ROOT_CONTEXT, time).unwrap(); + let before = canonical_bytes(&graph); + + let result = graph.transact(Proposal::add(route(12), ROOT_CONTEXT, left, right, length)); + + assert_eq!( + result, + Err(Obstruction::DimensionMismatch { + left: Dimension::LENGTH, + right: Dimension::TIME, + }) + ); + assert_eq!(canonical_bytes(&graph), before); + assert!(graph.resolve(&route(12)).is_none()); +} + +#[test] +fn multiplication_and_division_validate_derived_dimensions() { + let (mut graph, _, length, time, speed, area) = quantity_fixture(); + let left = graph.insert_value(route(20), ROOT_CONTEXT, length).unwrap(); + let right = graph.insert_value(route(21), ROOT_CONTEXT, length).unwrap(); + let duration = graph.insert_value(route(22), ROOT_CONTEXT, time).unwrap(); + + let product = graph + .transact(Proposal::multiply( + route(23), + ROOT_CONTEXT, + left, + right, + area, + )) + .unwrap(); + let quotient = graph + .transact(Proposal::divide( + route(24), + ROOT_CONTEXT, + left, + duration, + speed, + )) + .unwrap(); + + assert_eq!( + graph.node(product.evidence).unwrap().opcode(), + OpCode::KernelWitness + ); + assert_eq!( + graph.node(quotient.evidence).unwrap().opcode(), + OpCode::KernelWitness + ); + let bytes = canonical_bytes(&graph); + let decoded = decode_canonical(&bytes).unwrap(); + assert_eq!(canonical_bytes(&decoded), bytes); +} + +#[test] +fn decoder_rechecks_operation_law_instead_of_trusting_witness_shape() { + let (mut graph, _, length, time, speed, area) = quantity_fixture(); + let left = graph.insert_value(route(30), ROOT_CONTEXT, length).unwrap(); + let duration = graph.insert_value(route(31), ROOT_CONTEXT, time).unwrap(); + let committed = graph + .transact(Proposal::divide( + route(32), + ROOT_CONTEXT, + left, + duration, + speed, + )) + .unwrap(); + let mut bytes = canonical_bytes(&graph); + let node_record = 22 + committed.node as usize * 24; + bytes[node_record + 6..node_record + 10].copy_from_slice(&area.to_le_bytes()); + let judgment = committed.node + 1; + let conclusion_port = graph.node(judgment).unwrap().port_range().end - 1; + let port_section = 22 + graph.node_count() * 24; + let conclusion_record = port_section + conclusion_port * 8; + bytes[conclusion_record..conclusion_record + 4].copy_from_slice(&area.to_le_bytes()); + + assert_eq!( + decode_canonical(&bytes).unwrap_err(), + DecodeError::InvalidAuthority + ); +} + +#[test] +fn contradictory_context_is_rejected_before_context_mutation() { + let (mut graph, _, length, _, _, _) = quantity_fixture(); + let value = graph.insert_value(route(40), ROOT_CONTEXT, length).unwrap(); + let nonnegative = graph + .declare_constraint( + route(41), + ROOT_CONTEXT, + value, + ConstraintKind::NonNegative, + true, + ) + .unwrap(); + let positive_context = graph.extend_context(ROOT_CONTEXT, nonnegative).unwrap(); + let negative = graph + .declare_constraint( + route(42), + ROOT_CONTEXT, + value, + ConstraintKind::NonNegative, + false, + ) + .unwrap(); + let before = canonical_bytes(&graph); + let before_journal = graph.journal_len(); + + assert_eq!( + graph.extend_context(positive_context, negative), + Err(Obstruction::ContradictoryConstraint { + subject: value, + kind: ConstraintKind::NonNegative, + }) + ); + assert_eq!(canonical_bytes(&graph), before); + assert_eq!(graph.journal_len(), before_journal); +} + +#[test] +fn decoder_rejects_a_serialized_contradictory_context() { + let (mut graph, _, length, _, _, _) = quantity_fixture(); + let value = graph.insert_value(route(50), ROOT_CONTEXT, length).unwrap(); + let first = graph + .declare_constraint( + route(51), + ROOT_CONTEXT, + value, + ConstraintKind::NonNegative, + true, + ) + .unwrap(); + let first_context = graph.extend_context(ROOT_CONTEXT, first).unwrap(); + let second = graph + .declare_constraint( + route(52), + first_context, + value, + ConstraintKind::NonNegative, + true, + ) + .unwrap(); + graph.extend_context(first_context, second).unwrap(); + + let valid = canonical_bytes(&graph); + let decoded = decode_canonical(&valid).unwrap(); + assert_eq!(canonical_bytes(&decoded), valid); + + let mut contradictory = valid; + let second_record = 22 + second as usize * 24; + contradictory[second_record + 10..second_record + 18].copy_from_slice(&1_u64.to_le_bytes()); + assert_eq!( + decode_canonical(&contradictory).unwrap_err(), + DecodeError::InvalidAuthority + ); +} + +fn square_root_fixture() -> (Graph, u32, u32, u32) { + let mut graph = Graph::new(); + let real = graph.declare_atom_type(route(60), LocusWord(0x52)).unwrap(); + let length = graph + .declare_quantity_type(route(61), real, Dimension::LENGTH) + .unwrap(); + let area = graph + .declare_quantity_type( + route(62), + real, + Dimension::LENGTH.multiply(Dimension::LENGTH).unwrap(), + ) + .unwrap(); + let value = graph.insert_value(route(63), ROOT_CONTEXT, area).unwrap(); + (graph, value, length, area) +} + +#[test] +fn unresolved_square_root_guard_is_preserved_as_obligation() { + let (mut graph, value, length, _) = square_root_fixture(); + let committed = graph + .transact(Proposal::square_root( + route(64), + ROOT_CONTEXT, + value, + length, + )) + .unwrap(); + + let evidence = graph.node(committed.evidence).unwrap(); + assert_eq!(evidence.opcode(), OpCode::Obligation); + assert_eq!(graph.ports(committed.evidence).unwrap()[0].target(), value); + let bytes = canonical_bytes(&graph); + let decoded = decode_canonical(&bytes).unwrap(); + assert_eq!(canonical_bytes(&decoded), bytes); +} + +#[test] +fn discharged_square_root_guard_produces_kernel_witness() { + let (mut graph, value, length, _) = square_root_fixture(); + let condition = graph + .declare_constraint( + route(65), + ROOT_CONTEXT, + value, + ConstraintKind::NonNegative, + true, + ) + .unwrap(); + let context = graph.extend_context(ROOT_CONTEXT, condition).unwrap(); + let committed = graph + .transact(Proposal::square_root(route(66), context, value, length)) + .unwrap(); + + assert_eq!( + graph.node(committed.evidence).unwrap().opcode(), + OpCode::KernelWitness + ); +} + +#[test] +fn refuted_square_root_guard_rejects_without_mutation() { + let (mut graph, value, length, _) = square_root_fixture(); + let condition = graph + .declare_constraint( + route(67), + ROOT_CONTEXT, + value, + ConstraintKind::NonNegative, + false, + ) + .unwrap(); + let context = graph.extend_context(ROOT_CONTEXT, condition).unwrap(); + let before = canonical_bytes(&graph); + let before_journal = graph.journal_len(); + + assert_eq!( + graph.transact(Proposal::square_root(route(68), context, value, length)), + Err(Obstruction::GuardViolated { + subject: value, + kind: ConstraintKind::NonNegative, + }) + ); + assert_eq!(canonical_bytes(&graph), before); + assert_eq!(graph.journal_len(), before_journal); +} + +#[test] +fn decoder_refuses_a_forged_witness_for_an_unresolved_guard() { + let (mut graph, value, length, _) = square_root_fixture(); + let committed = graph + .transact(Proposal::square_root( + route(69), + ROOT_CONTEXT, + value, + length, + )) + .unwrap(); + let mut bytes = canonical_bytes(&graph); + let node_count = graph.node_count(); + let port_count = graph.port_count(); + let evidence_record = 22 + committed.evidence as usize * 24; + bytes[evidence_record..evidence_record + 2] + .copy_from_slice(&(OpCode::KernelWitness as u16).to_le_bytes()); + bytes[evidence_record + 22..evidence_record + 24].copy_from_slice(&0_u16.to_le_bytes()); + bytes[10..14].copy_from_slice(&((port_count - 1) as u32).to_le_bytes()); + let last_port = 22 + node_count * 24 + (port_count - 1) * 8; + bytes.drain(last_port..last_port + 8); + + assert_eq!( + decode_canonical(&bytes).unwrap_err(), + DecodeError::InvalidAuthority + ); +} diff --git a/l64-native/tests/dna.rs b/l64-native/tests/dna.rs new file mode 100644 index 0000000..7e4c45a --- /dev/null +++ b/l64-native/tests/dna.rs @@ -0,0 +1,79 @@ +use l64_native::{ + DnaError, Graph, LocusWord, Proposal, ROOT_CONTEXT, Route, decode_dna, dna_bytes, +}; + +fn route(index: u64) -> Route { + Route::root(LocusWord(0x4c36344e41544956)).composed(LocusWord(index)) +} + +fn proven_graph() -> Graph { + let mut graph = Graph::new(); + let a = graph.declare_atom_type(route(1), LocusWord(0x41)).unwrap(); + let b = graph.declare_atom_type(route(2), LocusWord(0x42)).unwrap(); + let c = graph.declare_atom_type(route(3), LocusWord(0x43)).unwrap(); + let ab = graph.declare_function_type(route(4), a, b).unwrap(); + let bc = graph.declare_function_type(route(5), b, c).unwrap(); + let ac = graph.declare_function_type(route(6), a, c).unwrap(); + let first = graph.insert_value(route(7), ROOT_CONTEXT, ab).unwrap(); + let second = graph.insert_value(route(8), ROOT_CONTEXT, bc).unwrap(); + graph + .transact(Proposal::compose(route(9), ROOT_CONTEXT, first, second, ac)) + .unwrap(); + graph +} + +#[test] +fn native_dna_roundtrip_is_exact_fixed_point() { + let graph = proven_graph(); + let dna = dna_bytes(&graph).unwrap(); + let decoded = decode_dna(&dna).unwrap(); + + assert_eq!(dna_bytes(&decoded).unwrap(), dna); + assert_eq!(decoded.state_commitment(), graph.state_commitment()); + assert_eq!(decoded.journal_len(), 0); + assert!(!dna.windows(b"claim".len()).any(|window| window == b"claim")); + assert!( + !dna.windows(b"theorem".len()) + .any(|window| window == b"theorem") + ); +} + +#[test] +fn native_dna_rejects_payload_tampering() { + let mut dna = dna_bytes(&proven_graph()).unwrap(); + let last = dna.len() - 1; + dna[last] ^= 1; + assert_eq!(decode_dna(&dna).unwrap_err(), DnaError::CommitmentMismatch); +} + +#[test] +fn native_dna_rejects_unknown_header_state() { + let original = dna_bytes(&proven_graph()).unwrap(); + + let mut version = original.clone(); + version[4..6].copy_from_slice(&2u16.to_le_bytes()); + assert_eq!( + decode_dna(&version).unwrap_err(), + DnaError::UnsupportedVersion { version: 2 } + ); + + let mut flags = original; + flags[6..8].copy_from_slice(&1u16.to_le_bytes()); + assert_eq!( + decode_dna(&flags).unwrap_err(), + DnaError::UnsupportedFlags { flags: 1 } + ); +} + +#[test] +fn native_dna_rejects_length_mismatch() { + let dna = dna_bytes(&proven_graph()).unwrap(); + assert_eq!( + decode_dna(&dna[..dna.len() - 1]).unwrap_err(), + DnaError::Truncated + ); + + let mut trailing = dna; + trailing.push(0); + assert_eq!(decode_dna(&trailing).unwrap_err(), DnaError::TrailingBytes); +} diff --git a/l64-native/tests/rna.rs b/l64-native/tests/rna.rs new file mode 100644 index 0000000..e8504a5 --- /dev/null +++ b/l64-native/tests/rna.rs @@ -0,0 +1,108 @@ +use l64_native::{ + ConstraintKind, Dimension, Obstruction, RnaError, compile_rna, dna_to_rna, normalize_rna, + rna_to_dna, +}; + +const CANONICAL_COMPOSE: &[u8] = b"L64R1 0x4c36344e41544956\na 1 0x41\na 2 0x42\na 3 0x43\nf 4 1 2\nf 5 2 3\nf 6 1 3\nv 7 4\nv 8 5\nc 9 7 8 6\n"; + +#[test] +fn native_rna_compiles_and_reaches_exact_dna_fixed_point() { + let graph = compile_rna(CANONICAL_COMPOSE).unwrap(); + assert_eq!(graph.node_count(), 11); + + let dna = rna_to_dna(CANONICAL_COMPOSE).unwrap(); + let sequenced = dna_to_rna(&dna).unwrap(); + assert_eq!(sequenced, CANONICAL_COMPOSE); + assert_eq!(rna_to_dna(&sequenced).unwrap(), dna); +} + +#[test] +fn native_rna_normalization_removes_surface_variation() { + let source = b"\n L64R1 0X4C36344E41544956\n a 1 0X41\n a 2 0x42\n a 3 0x43\n f 4 1 2\n f 5 2 3\n f 6 1 3\n v 7 4\n v 8 5\n c 9 7 8 6\n\n"; + assert_eq!(normalize_rna(source).unwrap(), CANONICAL_COMPOSE); +} + +#[test] +fn invalid_matrix_source_refuses_before_dna_exists() { + let source = b"L64R1 0x4c36344e41544956\na 1 0x52\nm 2 1 2 3\nm 3 1 4 2\nm 4 1 2 2\nv 5 2\nv 6 3\nx 7 5 6 4\n"; + assert_eq!( + rna_to_dna(source), + Err(RnaError::Graph(Obstruction::MatrixShapeMismatch { + left_cols: 3, + right_rows: 4, + })) + ); +} + +#[test] +fn native_rna_rejects_unknown_and_non_topological_slots() { + let unknown = b"L64R1 0x1\na 1 0x41\nf 2 1 9\n"; + assert_eq!( + compile_rna(unknown).unwrap_err(), + RnaError::UnknownSlot { line: 3, slot: 9 } + ); + + let unordered = b"L64R1 0x1\na 2 0x41\na 1 0x42\n"; + assert_eq!( + compile_rna(unordered).unwrap_err(), + RnaError::SlotOrder { line: 3, slot: 1 } + ); +} + +#[test] +fn native_rna_rejects_record_like_or_named_instructions() { + let source = b"L64R1 0x1\nclaim 1 2\n"; + assert_eq!( + compile_rna(source).unwrap_err(), + RnaError::UnknownInstruction { line: 2 } + ); +} + +const CONSTRAINT_RNA: &[u8] = b"L64R1 0x434f4e5354524149\na 1 0x52\nq 2 1 1 0 0 0 0 0 0\nq 3 1 2 0 0 0 0 0 0\nv 4 3\nr 5 4 2\nk 6 4 1 1\nh 1 0 6\nr 7 4 2 1\n"; + +#[test] +fn constraints_contexts_and_obligations_reach_exact_rna_dna_fixed_point() { + let graph = compile_rna(CONSTRAINT_RNA).unwrap(); + assert_eq!(graph.context_count(), 2); + let dna = rna_to_dna(CONSTRAINT_RNA).unwrap(); + let sequenced = dna_to_rna(&dna).unwrap(); + assert_eq!(sequenced, CONSTRAINT_RNA); + assert_eq!(rna_to_dna(&sequenced).unwrap(), dna); + assert!(!sequenced.windows(2).any(|window| window == b"{:")); +} + +#[test] +fn rna_preserves_unknown_guard_and_refuses_refuted_guard() { + let refuted = b"L64R1 0x434f4e5354524149\na 1 0x52\nq 2 1 1 0 0 0 0 0 0\nq 3 1 2 0 0 0 0 0 0\nv 4 3\nk 5 4 1 0\nh 1 0 5\nr 6 4 2 1\n"; + assert_eq!( + rna_to_dna(refuted), + Err(RnaError::Graph(Obstruction::GuardViolated { + subject: 3, + kind: ConstraintKind::NonNegative, + })) + ); +} + +#[test] +fn rna_rejects_dimensionally_invalid_addition() { + let source = b"L64R1 0x434f4e5354524149\na 1 0x52\nq 2 1 1 0 0 0 0 0 0\nq 3 1 0 0 1 0 0 0 0\nv 4 2\nv 5 3\n+ 6 4 5 2\n"; + assert_eq!( + rna_to_dna(source), + Err(RnaError::Graph(Obstruction::DimensionMismatch { + left: Dimension::LENGTH, + right: Dimension::TIME, + })) + ); +} + +#[test] +fn rna_rejects_nonsequential_context_ids() { + let source = b"L64R1 0x1\na 1 0x52\nq 2 1 0 0 0 0 0 0 0\nv 3 2\nk 4 3 1 1\nh 2 0 4\n"; + assert_eq!( + compile_rna(source).unwrap_err(), + RnaError::ContextOrder { + line: 6, + context: 2, + } + ); +} diff --git a/l64-native/tests/workloads.rs b/l64-native/tests/workloads.rs new file mode 100644 index 0000000..77ff234 --- /dev/null +++ b/l64-native/tests/workloads.rs @@ -0,0 +1,114 @@ +use l64_native::{Graph, LocusWord, Obstruction, OpCode, Proposal, ROOT_CONTEXT, Route}; + +fn route(index: u64) -> Route { + Route::root(LocusWord(0x4c36344e41544956)).composed(LocusWord(index)) +} + +#[test] +fn valid_typed_function_composition_commits() { + let mut graph = Graph::new(); + let a = graph.declare_atom_type(route(1), LocusWord(0x41)).unwrap(); + let b = graph.declare_atom_type(route(2), LocusWord(0x42)).unwrap(); + let c = graph.declare_atom_type(route(3), LocusWord(0x43)).unwrap(); + let ab = graph.declare_function_type(route(4), a, b).unwrap(); + let bc = graph.declare_function_type(route(5), b, c).unwrap(); + let ac = graph.declare_function_type(route(6), a, c).unwrap(); + let first = graph.insert_value(route(7), ROOT_CONTEXT, ab).unwrap(); + let second = graph.insert_value(route(8), ROOT_CONTEXT, bc).unwrap(); + let before = graph.state_commitment(); + let before_nodes = graph.node_count(); + + let committed = graph + .transact(Proposal::compose(route(9), ROOT_CONTEXT, first, second, ac)) + .unwrap(); + + assert_eq!(graph.node_count(), before_nodes + 3); + assert_ne!(graph.state_commitment(), before); + assert_eq!(graph.node(committed.node).unwrap().ty(), Some(ac)); + assert_eq!(graph.ports(committed.node).unwrap().len(), 2); + let evidence = graph.node(committed.evidence).unwrap(); + assert_eq!(evidence.opcode(), OpCode::KernelWitness); + let judgment = evidence.ty().unwrap(); + assert_eq!(graph.node(judgment).unwrap().opcode(), OpCode::TypeJudgment); + assert_eq!(graph.ports(judgment).unwrap().len(), 4); + assert_eq!( + graph.journal().last().unwrap().after(), + committed.commitment + ); +} + +#[test] +fn invalid_matrix_shape_rejects_without_mutation() { + let mut graph = Graph::new(); + let scalar = graph.declare_atom_type(route(11), LocusWord(0x52)).unwrap(); + let left_ty = graph.declare_matrix_type(route(12), scalar, 2, 3).unwrap(); + let right_ty = graph.declare_matrix_type(route(13), scalar, 4, 2).unwrap(); + let output_ty = graph.declare_matrix_type(route(14), scalar, 2, 2).unwrap(); + let left = graph + .insert_value(route(15), ROOT_CONTEXT, left_ty) + .unwrap(); + let right = graph + .insert_value(route(16), ROOT_CONTEXT, right_ty) + .unwrap(); + + let before_commitment = graph.state_commitment(); + let before_nodes = graph.node_count(); + let before_ports = graph.port_count(); + let before_contexts = graph.context_count(); + let before_journal = graph.journal_len(); + + let result = graph.transact(Proposal::matrix_multiply( + route(17), + ROOT_CONTEXT, + left, + right, + output_ty, + )); + + assert_eq!( + result, + Err(Obstruction::MatrixShapeMismatch { + left_cols: 3, + right_rows: 4, + }) + ); + assert_eq!(graph.state_commitment(), before_commitment); + assert_eq!(graph.node_count(), before_nodes); + assert_eq!(graph.port_count(), before_ports); + assert_eq!(graph.context_count(), before_contexts); + assert_eq!(graph.journal_len(), before_journal); + assert!(graph.resolve(&route(17)).is_none()); +} + +#[test] +fn kernel_judgment_cannot_be_forged_through_value_insertion() { + let mut graph = Graph::new(); + let a = graph.declare_atom_type(route(31), LocusWord(0x41)).unwrap(); + let b = graph.declare_atom_type(route(32), LocusWord(0x42)).unwrap(); + let c = graph.declare_atom_type(route(33), LocusWord(0x43)).unwrap(); + let ab = graph.declare_function_type(route(34), a, b).unwrap(); + let bc = graph.declare_function_type(route(35), b, c).unwrap(); + let ac = graph.declare_function_type(route(36), a, c).unwrap(); + let first = graph.insert_value(route(37), ROOT_CONTEXT, ab).unwrap(); + let second = graph.insert_value(route(38), ROOT_CONTEXT, bc).unwrap(); + let committed = graph + .transact(Proposal::compose( + route(39), + ROOT_CONTEXT, + first, + second, + ac, + )) + .unwrap(); + let judgment = graph.node(committed.evidence).unwrap().ty().unwrap(); + let before = graph.state_commitment(); + let before_nodes = graph.node_count(); + + assert_eq!( + graph.insert_value(route(40), ROOT_CONTEXT, judgment), + Err(Obstruction::EvidenceOnlyType { node: judgment }) + ); + assert_eq!(graph.state_commitment(), before); + assert_eq!(graph.node_count(), before_nodes); + assert!(graph.resolve(&route(40)).is_none()); +}