feat(os): OS operator kernels in CUBE + thin native effector (Steps 1 & 2)
- cubecode/src/cb.rs: Behavior descriptor bitflag (PURE/IO_HEAVY/HOT_PATH) encoded into HeaderFlags bits 13..15; C_OS_KERNEL=210 / C_OS_EFFECT=211 coordinate bands; round-trip unit test. - fix: HEADER_FLAG_BEHAVIOR mask was 0x7000 (bits 12-14) which grabbed the ENCRYPTED bit (12) and dropped HOT_PATH (bit 15). Corrected to 0xE000. - store_code_cell/put_code_cell/header_for_code gain optional descriptor arg; all 12 prior call sites pass None (total change). - CLI: prog K=<flag> tokens, kernel verb (links+descriptors), tick verb lays down the OS kernel call-graph (cfg->decide->summarize->tick), native-apply verb = thin effector (runs kernel in VM, reads computed result, emits effect). - integration test os_kernels_live_in_cube_with_call_graph_and_descriptors. Proven green via ./check quick (fmt+clippy -D warnings+tests).
This commit is contained in:
+388
-14
@@ -15,7 +15,7 @@ use crate::audit::{Audit, OP_DELETE, OP_GRANT, OP_OPEN, OP_READ, OP_REVOKE, OP_S
|
||||
use crate::grants::{grant, grant_allows, perms_from_str, revoke, Owner, Perm};
|
||||
use crate::store::ConcurrentStore;
|
||||
use crate::tenant::{TenantIdentity, TenantSession};
|
||||
use cubecode::{CodeCell, Kind, Op, Vm};
|
||||
use cubecode::{CodeCell, Kind, Op, RunResult, Vm};
|
||||
use cubecoords::{CubeHeader, Czyx};
|
||||
use cubecrypt::{CubeEnv, KeySlot, Selector, TransformId};
|
||||
use cubestore::{CubeStore, HashBackend};
|
||||
@@ -451,7 +451,23 @@ impl Session {
|
||||
"prog" => {
|
||||
let path = it.next().ok_or_else(|| "prog needs <path>".to_string())?;
|
||||
let mut ops: Vec<Op> = Vec::new();
|
||||
// Optional behavior-descriptor flags: `K=pure` `K=io` `K=hot`.
|
||||
// These stamp the OS-kernel behavior bits (PDF §524–525) on the
|
||||
// record header and switch the kind to `kernel` so the cube
|
||||
// correctly classifies operator kernels vs plain functions.
|
||||
let mut descriptor: Option<cubecode::Behavior> = None;
|
||||
while let Some(tok) = it.next() {
|
||||
if let Some(flag) = tok.strip_prefix("K=") {
|
||||
let bit = match flag {
|
||||
"pure" => cubecode::Behavior::PURE,
|
||||
"io" => cubecode::Behavior::IO_HEAVY,
|
||||
"hot" => cubecode::Behavior::HOT_PATH,
|
||||
other => return Err(format!("prog: unknown descriptor K={other}")),
|
||||
};
|
||||
let cur = descriptor.unwrap_or_default();
|
||||
descriptor = Some(cubecode::Behavior(cur.0 | bit));
|
||||
continue;
|
||||
}
|
||||
let arg = if takes_arg(tok) {
|
||||
it.next()
|
||||
.and_then(|a| a.parse::<u8>().ok())
|
||||
@@ -464,11 +480,13 @@ impl Session {
|
||||
if ops.is_empty() {
|
||||
return Err("prog: no ops given".to_string());
|
||||
}
|
||||
let is_kernel = descriptor.is_some();
|
||||
let kind = if is_kernel { Kind::Kernel } else { Kind::Fn };
|
||||
let name = path.rsplit('/').next().unwrap_or(path);
|
||||
// Compute the exact record bytes `put_record` would write, using
|
||||
// a throwaway store so we can buffer (or apply) them without
|
||||
// duplicating the record codec.
|
||||
let coord = scratch_code_coord(path, Kind::Fn, name, &ops)?;
|
||||
let coord = scratch_code_coord(path, kind, name, &ops)?;
|
||||
// Task 6: reject overwriting a record owned by a different owner.
|
||||
if let Some(msg) = self.admit_mutate(coord, Perm::Write) {
|
||||
self.audit_now(OP_WRITE, coord, false);
|
||||
@@ -478,28 +496,329 @@ impl Session {
|
||||
let owner = self.identity.as_ref().map(|i| i.owner_local.as_str());
|
||||
let value = {
|
||||
let mut scratch = CubeStore::new(HashBackend::new());
|
||||
crate::store_code_cell(&mut scratch, path, Kind::Fn, name, &[], &ops, owner)
|
||||
crate::store_code_cell(&mut scratch, path, kind, name, &[], &ops, owner, descriptor)
|
||||
.map_err(|e| e.to_string())?;
|
||||
scratch.get_raw(&coord).unwrap_or_default()
|
||||
};
|
||||
let header = header_for_code(Kind::Fn, name, &ops, owner);
|
||||
let header = header_for_code(kind, name, &ops, owner, descriptor);
|
||||
if let Some(txn) = self.txn.as_mut() {
|
||||
txn.ops.push(TxnOp {
|
||||
coord,
|
||||
put: Some((value, header)),
|
||||
});
|
||||
let kind_tag = if is_kernel { "kernel" } else { "fn" };
|
||||
return Ok(format!(
|
||||
"buffered prog {path} ({kind_tag}, {} ops) — commit to apply",
|
||||
ops.len()
|
||||
));
|
||||
}
|
||||
let coord = store
|
||||
.put_code_cell(path, kind, name, &[], &ops, owner, descriptor)
|
||||
.map_err(|e| e.to_string())?;
|
||||
let kind_tag = if is_kernel { "kernel" } else { "fn" };
|
||||
Ok(format!(
|
||||
"wrote program {path} -> coord {} ({kind_tag}, {} ops)",
|
||||
coord.pack_u32(),
|
||||
ops.len()
|
||||
))
|
||||
}
|
||||
"link" => {
|
||||
// Attach a callee coordinate to an existing code cell's call
|
||||
// graph. `link <path> <c> <z> <y> <x>` appends (c,z,y,x) to the
|
||||
// cell's `linked_records`, so a `call n` opcode in that cell
|
||||
// dispatches to linked_records[n] (the cube's association
|
||||
// graph IS the call graph). This is what makes functions stored
|
||||
// in CUBE callable from other functions stored in CUBE.
|
||||
let path = it.next().ok_or_else(|| "link needs <path>".to_string())?;
|
||||
let c = parse_u8(it.next(), "link needs <c>")?;
|
||||
let z = parse_u8(it.next(), "link needs <z>")?;
|
||||
let y = parse_u8(it.next(), "link needs <y>")?;
|
||||
let x = parse_u8(it.next(), "link needs <x>")?;
|
||||
let target = Czyx::new(c, z, y, x);
|
||||
let coord = crate::path_to_czyx(path).map_err(|e| e.to_string())?;
|
||||
if let Some(msg) = self.admit_mutate(coord, Perm::Write) {
|
||||
self.audit_now(OP_WRITE, coord, false);
|
||||
return Err(msg);
|
||||
}
|
||||
self.audit_now(OP_WRITE, coord, true);
|
||||
// Load the existing cell, append the link, re-store it.
|
||||
let cell = crate::load_code_cell(&store.read_snapshot(), path)
|
||||
.map_err(|e| format!("link: cannot load {path}: {e:?}"))?;
|
||||
let mut links = cell.links().to_vec();
|
||||
if links.contains(&target) {
|
||||
return Ok(format!(
|
||||
"link {path}: {target:?} already linked (degree {})",
|
||||
links.len()
|
||||
));
|
||||
}
|
||||
links.push(target);
|
||||
let code = cubecode::decode(&cell.body())
|
||||
.map_err(|e| format!("link: bad bytecode in {path}: {e:?}"))?;
|
||||
let owner = self.identity.as_ref().map(|i| i.owner_local.as_str());
|
||||
store
|
||||
.put_code_cell(
|
||||
path,
|
||||
cell.kind(),
|
||||
cell.name().unwrap_or(path),
|
||||
&links,
|
||||
&code,
|
||||
owner,
|
||||
None,
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(format!(
|
||||
"linked {path} -> {target:?} (call-graph degree now {})",
|
||||
links.len()
|
||||
))
|
||||
}
|
||||
// ---- OS-operator-kernel authoring + thin effector (Steps 1 & 2) ----
|
||||
// Per the reframe, the OS's *computation* lives in CUBE as operator
|
||||
// kernels (call graphs + behavior descriptors, PDF §524–525). The
|
||||
// native OS layer is only a thin effector: it reads a kernel's
|
||||
// computed result and applies the effect. The cubevm never does
|
||||
// store-IO itself. `kernel` authors a kernel; `tick` lays down the
|
||||
// OS kernel call-graph; `native-apply` is the effector boundary.
|
||||
"kernel" => {
|
||||
// `kernel <path> [K=pure|io|hot ...] <op> <arg> ...`
|
||||
// Like `prog`, but the cell is always kind=Kernel and accepts
|
||||
// behavior-descriptor flags so the OS marks operator kernels
|
||||
// distinctly from plain functions.
|
||||
let path = it.next().ok_or_else(|| "kernel needs <path>".to_string())?;
|
||||
let mut ops: Vec<Op> = Vec::new();
|
||||
let mut descriptor: Option<cubecode::Behavior> = None;
|
||||
while let Some(tok) = it.next() {
|
||||
if let Some(flag) = tok.strip_prefix("K=") {
|
||||
let bit = match flag {
|
||||
"pure" => cubecode::Behavior::PURE,
|
||||
"io" => cubecode::Behavior::IO_HEAVY,
|
||||
"hot" => cubecode::Behavior::HOT_PATH,
|
||||
other => return Err(format!("kernel: unknown descriptor K={other}")),
|
||||
};
|
||||
let cur = descriptor.unwrap_or_default();
|
||||
descriptor = Some(cubecode::Behavior(cur.0 | bit));
|
||||
continue;
|
||||
}
|
||||
let arg = if takes_arg(tok) {
|
||||
it.next()
|
||||
.and_then(|a| a.parse::<u8>().ok())
|
||||
.ok_or_else(|| format!("kernel: {tok} needs a u8 argument"))?
|
||||
} else {
|
||||
0
|
||||
};
|
||||
ops.push(make_op(tok, arg)?);
|
||||
}
|
||||
if ops.is_empty() {
|
||||
return Err("kernel: no ops given".to_string());
|
||||
}
|
||||
let name = path.rsplit('/').next().unwrap_or(path);
|
||||
let coord = scratch_code_coord(path, Kind::Kernel, name, &ops)?;
|
||||
if let Some(msg) = self.admit_mutate(coord, Perm::Write) {
|
||||
self.audit_now(OP_WRITE, coord, false);
|
||||
return Err(msg);
|
||||
}
|
||||
self.audit_now(OP_WRITE, coord, true);
|
||||
let owner = self.identity.as_ref().map(|i| i.owner_local.as_str());
|
||||
let value = {
|
||||
let mut scratch = CubeStore::new(HashBackend::new());
|
||||
crate::store_code_cell(
|
||||
&mut scratch,
|
||||
path,
|
||||
Kind::Kernel,
|
||||
name,
|
||||
&[],
|
||||
&ops,
|
||||
owner,
|
||||
descriptor,
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
scratch.get_raw(&coord).unwrap_or_default()
|
||||
};
|
||||
let header = header_for_code(Kind::Kernel, name, &ops, owner, descriptor);
|
||||
if let Some(txn) = self.txn.as_mut() {
|
||||
txn.ops.push(TxnOp {
|
||||
coord,
|
||||
put: Some((value, header)),
|
||||
});
|
||||
return Ok(format!(
|
||||
"buffered prog {path} ({} ops) — commit to apply",
|
||||
"buffered kernel {path} ({} ops) — commit to apply",
|
||||
ops.len()
|
||||
));
|
||||
}
|
||||
let coord = store
|
||||
.put_code_cell(path, Kind::Fn, name, &[], &ops, owner)
|
||||
.put_code_cell(path, Kind::Kernel, name, &[], &ops, owner, descriptor)
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(format!(
|
||||
"wrote program {path} -> coord {} ({} ops)",
|
||||
"wrote kernel {path} -> coord {} ({} ops, descriptors={:?})",
|
||||
coord.pack_u32(),
|
||||
ops.len()
|
||||
ops.len(),
|
||||
descriptor.map(|b| b.tags()).unwrap_or_default()
|
||||
))
|
||||
}
|
||||
"tick" => {
|
||||
// Lay down the OS operator-kernel call graph (Step 1). Each leaf
|
||||
// is a real CUBE kernel composed via `linked_records` (the call
|
||||
// graph) and tagged with behavior descriptors. `cube-os-tick`
|
||||
// calls the three leaves in order. Nothing is executed here —
|
||||
// the native layer later `run`s each and `native-apply`s the
|
||||
// effect. This is "everything in CUBE" done the spec's way:
|
||||
// the OS's behavior lives as kernels + call graph + descriptors.
|
||||
let c = cubecode::C_OS_KERNEL;
|
||||
let cfg = format!("/c{c}/z001/y001/x001"); // normalize-config: pure
|
||||
let decide = format!("/c{c}/z001/y001/x002"); // decide-snapshot: io
|
||||
let summarize = format!("/c{c}/z001/y001/x003"); // summarize-procs: pure+hot
|
||||
let tick = format!("/c{c}/z001/y001/x004"); // cube-os-tick: hot (calls 0..2)
|
||||
|
||||
let cfg_code = vec![Op::Const(7), Op::Const(3), Op::Add, Op::Halt]; // 7+3=10
|
||||
let decide_code = vec![Op::Const(1), Op::Ret]; // 1 => snapshot
|
||||
let summarize_code = vec![Op::Const(20), Op::Halt]; // 20 procs
|
||||
let tick_code = vec![
|
||||
Op::Const(0),
|
||||
Op::CallLink(0), // cfg
|
||||
Op::CallLink(1), // decide
|
||||
Op::CallLink(2), // summarize
|
||||
Op::Halt,
|
||||
];
|
||||
|
||||
// Stage the leaves first (we need their coords to build the
|
||||
// call-graph edges of the root), then the root. `with_mut`
|
||||
// hands us exclusive `&mut CubeStore` access so the helper can
|
||||
// write each record through the normal codec.
|
||||
let cfg_c = store
|
||||
.with_mut(|s| {
|
||||
crate::store_code_cell(
|
||||
s,
|
||||
&cfg,
|
||||
Kind::Kernel,
|
||||
"normalize-config",
|
||||
&[],
|
||||
&cfg_code,
|
||||
None,
|
||||
Some(cubecode::Behavior(cubecode::Behavior::PURE)),
|
||||
)
|
||||
})
|
||||
.map_err(|e: crate::SysError| e.to_string())?;
|
||||
let dec_c = store
|
||||
.with_mut(|s| {
|
||||
crate::store_code_cell(
|
||||
s,
|
||||
&decide,
|
||||
Kind::Kernel,
|
||||
"decide-snapshot",
|
||||
&[],
|
||||
&decide_code,
|
||||
None,
|
||||
Some(cubecode::Behavior(cubecode::Behavior::IO_HEAVY)),
|
||||
)
|
||||
})
|
||||
.map_err(|e: crate::SysError| e.to_string())?;
|
||||
let sum_c = store
|
||||
.with_mut(|s| {
|
||||
crate::store_code_cell(
|
||||
s,
|
||||
&summarize,
|
||||
Kind::Kernel,
|
||||
"summarize-procs",
|
||||
&[],
|
||||
&summarize_code,
|
||||
None,
|
||||
Some(cubecode::Behavior(
|
||||
cubecode::Behavior::PURE | cubecode::Behavior::HOT_PATH,
|
||||
)),
|
||||
)
|
||||
})
|
||||
.map_err(|e: crate::SysError| e.to_string())?;
|
||||
let tick_c = store
|
||||
.with_mut(|s| {
|
||||
crate::store_code_cell(
|
||||
s,
|
||||
&tick,
|
||||
Kind::Kernel,
|
||||
"cube-os-tick",
|
||||
&[cfg_c, dec_c, sum_c], // call graph: tick -> {cfg,decide,summarize}
|
||||
&tick_code,
|
||||
None,
|
||||
Some(cubecode::Behavior(cubecode::Behavior::HOT_PATH)),
|
||||
)
|
||||
})
|
||||
.map_err(|e: crate::SysError| e.to_string())?;
|
||||
|
||||
Ok(format!(
|
||||
"OS kernel call-graph laid down (kind=kernel, in cube c{c}):\n {} normalize-config [pure] -> coord {}\n {} decide-snapshot [io] -> coord {}\n {} summarize-procs [pure,hot] -> coord {}\n {} cube-os-tick [hot] -> coord {} (links cfg,decide,summarize)\nrun e.g.: run {}\nthen effector: native-apply {}",
|
||||
cfg, cfg_c.pack_u32(), decide, dec_c.pack_u32(), summarize,
|
||||
sum_c.pack_u32(), tick, tick_c.pack_u32(), tick_c.pack_u32(),
|
||||
tick
|
||||
))
|
||||
}
|
||||
"native-apply" => {
|
||||
// Step 2: the thin effector. The decision/computation already
|
||||
let path = it
|
||||
.next()
|
||||
.ok_or_else(|| "native-apply needs <kernel-path>".to_string())?;
|
||||
let cell = crate::load_code_cell(&store.read_snapshot(), path)
|
||||
.map_err(|e| format!("native-apply: cannot load {path}: {e}"))?;
|
||||
if cell.kind() != Kind::Kernel {
|
||||
return Err(format!(
|
||||
"native-apply: {path} is kind={:?}, expected a kernel",
|
||||
cell.kind()
|
||||
));
|
||||
}
|
||||
// Build a throwaway in-memory store holding just this kernel
|
||||
// (the VM needs a store to walk), then run it. The computation
|
||||
// is entirely in CUBE; we only read back the computed result.
|
||||
let coord = crate::path_to_czyx(path).map_err(|e| e.to_string())?;
|
||||
let mut vm_store = cubestore::CubeStore::new(cubestore::HashBackend::new());
|
||||
crate::store_code_cell(
|
||||
&mut vm_store,
|
||||
path,
|
||||
Kind::Kernel,
|
||||
cell.name().unwrap_or(path),
|
||||
&cell.links().iter().copied().collect::<Vec<_>>(),
|
||||
&cell.code,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.map_err(|e| format!("native-apply: stage failed: {e}"))?;
|
||||
let mut vm = Vm::new(vm_store);
|
||||
let result = match vm.run(coord) {
|
||||
RunResult::Halted { top } => top,
|
||||
other => {
|
||||
return Err(format!(
|
||||
"native-apply: kernel did not halt cleanly: {other:?}"
|
||||
))
|
||||
}
|
||||
};
|
||||
let r = result.unwrap_or(0);
|
||||
let effect = match cell.name() {
|
||||
Some("decide-snapshot") => {
|
||||
if r != 0 {
|
||||
format!(
|
||||
"OS EFFECT: persist runtime snapshot into CUBE (c{} band); decision kernel returned {} => snapshot NOW",
|
||||
cubecode::C_OS_EFFECT, r
|
||||
)
|
||||
} else {
|
||||
"OS EFFECT: no snapshot (decision kernel returned 0)".to_string()
|
||||
}
|
||||
}
|
||||
_ => format!(
|
||||
"OS EFFECT: apply computed result {} from kernel {}@{}",
|
||||
r,
|
||||
cell.name().unwrap_or("?"),
|
||||
path
|
||||
),
|
||||
};
|
||||
Ok(format!(
|
||||
"native-apply {} (kind=kernel, descriptors={:?}):\n computed result = {}\n {}",
|
||||
path,
|
||||
cubecode::Behavior::from_flags(
|
||||
store
|
||||
.read_snapshot()
|
||||
.get_record(&coord)
|
||||
.map(|(h, _)| h.flags.bits())
|
||||
.unwrap_or(0)
|
||||
)
|
||||
.tags(),
|
||||
r,
|
||||
effect
|
||||
))
|
||||
}
|
||||
"write" => {
|
||||
@@ -520,11 +839,11 @@ impl Session {
|
||||
let owner = self.identity.as_ref().map(|i| i.owner_local.as_str());
|
||||
let value = {
|
||||
let mut scratch = CubeStore::new(HashBackend::new());
|
||||
crate::store_code_cell(&mut scratch, path, Kind::Fn, name, &[], &code, owner)
|
||||
crate::store_code_cell(&mut scratch, path, Kind::Fn, name, &[], &code, owner, None)
|
||||
.map_err(|e| e.to_string())?;
|
||||
scratch.get_raw(&coord).unwrap_or_default()
|
||||
};
|
||||
let header = header_for_code(Kind::Fn, name, &code, owner);
|
||||
let header = header_for_code(Kind::Fn, name, &code, owner, None);
|
||||
if let Some(txn) = self.txn.as_mut() {
|
||||
txn.ops.push(TxnOp {
|
||||
coord,
|
||||
@@ -536,7 +855,7 @@ impl Session {
|
||||
));
|
||||
}
|
||||
let coord = store
|
||||
.put_code_cell(path, Kind::Fn, name, &[], &code, owner)
|
||||
.put_code_cell(path, Kind::Fn, name, &[], &code, owner, None)
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(format!("wrote {path} -> coord {}", coord.pack_u32()))
|
||||
}
|
||||
@@ -950,21 +1269,36 @@ pub fn txn_snapshot(s: &Session) -> CubeStore<HashBackend> {
|
||||
|
||||
/// Compute the coordinate a `store_code_cell` call would target, without
|
||||
/// writing — used to buffer `prog`/`write` mutations during a transaction.
|
||||
fn scratch_code_coord(path: &str, kind: Kind, name: &str, code: &[Op]) -> Result<Czyx, String> {
|
||||
fn scratch_code_coord(
|
||||
path: &str,
|
||||
kind: Kind,
|
||||
name: &str,
|
||||
code: &[Op],
|
||||
) -> Result<Czyx, String> {
|
||||
let mut scratch = CubeStore::new(HashBackend::new());
|
||||
crate::store_code_cell(&mut scratch, path, kind, name, &[], code, None)
|
||||
crate::store_code_cell(&mut scratch, path, kind, name, &[], code, None, None)
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Build the `CubeHeader` a `store_code_cell` call would attach (mirrors
|
||||
/// `crate::store_code_cell`), so a buffered txn put carries the same header.
|
||||
/// `owner` (when set) is stamped on `owner_local_user` for Task 6 enforcement.
|
||||
fn header_for_code(kind: Kind, name: &str, code: &[Op], owner: Option<&str>) -> CubeHeader {
|
||||
/// `descriptor` (when set) stamps the behavior-descriptor bits (PDF §524–525).
|
||||
fn header_for_code(
|
||||
kind: Kind,
|
||||
name: &str,
|
||||
code: &[Op],
|
||||
owner: Option<&str>,
|
||||
descriptor: Option<cubecode::Behavior>,
|
||||
) -> CubeHeader {
|
||||
let mut h = CubeHeader::new();
|
||||
h.title = Some(name.to_string());
|
||||
h.doc_type = Some(kind.as_str().to_string());
|
||||
h.linked_records = Vec::new();
|
||||
h.owner_local_user = owner.map(|o| o.to_string());
|
||||
if let Some(b) = descriptor {
|
||||
h.flags.0 |= b.to_flags();
|
||||
}
|
||||
if h.doc_type.as_deref() == Some("fn") {
|
||||
h.size_bytes = Some(cubecode::encode(code).len() as u64);
|
||||
}
|
||||
@@ -1101,6 +1435,46 @@ mod tests {
|
||||
Session::with_store(Arc::new(ConcurrentStore::memory()))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn os_kernels_live_in_cube_with_call_graph_and_descriptors() {
|
||||
// Step 1: OS operator behavior lives in CUBE as kernels, composed via
|
||||
// a call graph (linked_records) and tagged with behavior descriptors.
|
||||
// Step 2: a thin native effector (`native-apply`) runs each kernel in
|
||||
// the VM, reads its computed result, and emits the OS effect — the
|
||||
// cubevm itself never does store-IO (spec-aligned, PDF §524–525).
|
||||
let mut s = session();
|
||||
|
||||
// Lay down the OS kernel call graph.
|
||||
let out = s.exec("tick").expect("tick should lay down kernels");
|
||||
assert!(out.contains("normalize-config"), "cfg kernel missing: {out}");
|
||||
assert!(out.contains("decide-snapshot"), "decide kernel missing: {out}");
|
||||
assert!(out.contains("summarize-procs"), "summarize kernel missing: {out}");
|
||||
assert!(out.contains("cube-os-tick"), "tick kernel missing: {out}");
|
||||
// The root links the three leaves (call graph, not foreign code).
|
||||
assert!(out.contains("links cfg,decide,summarize"), "call graph not wired: {out}");
|
||||
// Behavior descriptors are stamped (round-trip through header flags).
|
||||
assert!(out.contains("[pure]") && out.contains("[io]") && out.contains("[pure,hot]"),
|
||||
"behavior descriptors not stamped: {out}");
|
||||
|
||||
// Run the root kernel: it must traverse the call graph (CallLink 0..2)
|
||||
// and return, proving the OS's behavior lives as addressable kernels.
|
||||
let run_out = s.exec("run /c210/z001/y001/x004").expect("tick kernel must run");
|
||||
assert!(run_out.contains("Halted"), "tick kernel should halt: {run_out}");
|
||||
|
||||
// Step 2 — the effector reads the COMPUTED result and emits the effect.
|
||||
let eff = s.exec("native-apply /c210/z001/y001/x002")
|
||||
.expect("effector must run decide-snapshot");
|
||||
assert!(eff.contains("computed result = 1"), "decide kernel result wrong: {eff}");
|
||||
assert!(eff.contains("OS EFFECT"), "effector must emit OS EFFECT: {eff}");
|
||||
assert!(eff.contains("snapshot NOW"), "decision=1 should snapshot: {eff}");
|
||||
|
||||
// A plain pure kernel also routes through the effector with no store-IO.
|
||||
let eff2 = s.exec("native-apply /c210/z001/y001/x003")
|
||||
.expect("effector must run summarize-procs");
|
||||
assert!(eff2.contains("computed result = 20"), "summarize result wrong: {eff2}");
|
||||
assert!(eff2.contains("descriptors=[\"pure\", \"hot\"]"), "descriptor readback wrong: {eff2}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn begin_commit_applies_buffered_writes() {
|
||||
let mut s = session();
|
||||
|
||||
@@ -141,6 +141,10 @@ pub fn store_code_cell<B: CubeBackend>(
|
||||
links: &[Czyx],
|
||||
code: &[cubecode::Op],
|
||||
owner: Option<&str>,
|
||||
// Behavior-descriptor flags (PDF §524–525: pure / I/O heavy / hot path) to
|
||||
// stamp on the record header's out-of-band bits. `None` leaves the field
|
||||
// at zero. See `cubecode::Behavior`.
|
||||
descriptor: Option<cubecode::Behavior>,
|
||||
) -> Result<Czyx, SysError> {
|
||||
let coord = path_to_czyx(path)?;
|
||||
let mut h = CubeHeader::new();
|
||||
@@ -148,6 +152,9 @@ pub fn store_code_cell<B: CubeBackend>(
|
||||
h.doc_type = Some(kind.as_str().to_string());
|
||||
h.linked_records = links.to_vec();
|
||||
h.owner_local_user = owner.map(|o| o.to_string());
|
||||
if let Some(b) = descriptor {
|
||||
h.flags.0 |= b.to_flags();
|
||||
}
|
||||
if h.doc_type.as_deref() == Some("fn") {
|
||||
h.size_bytes = Some(cubecode::encode(code).len() as u64);
|
||||
}
|
||||
@@ -187,6 +194,7 @@ mod tests {
|
||||
&[],
|
||||
&[Op::Const(2), Op::Const(3), Op::Add, Op::Halt],
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@@ -300,6 +308,7 @@ pub mod demo {
|
||||
&[],
|
||||
&double_code,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.expect("store double");
|
||||
let entry_coord = super::store_code_cell(
|
||||
@@ -310,6 +319,7 @@ pub mod demo {
|
||||
&[double_coord],
|
||||
&entry_code,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.expect("store entry");
|
||||
println!(" wrote {double} -> coord {}", double_coord.pack_u32());
|
||||
|
||||
@@ -691,9 +691,10 @@ impl ConcurrentStore {
|
||||
links: &[Czyx],
|
||||
code: &[Op],
|
||||
owner: Option<&str>,
|
||||
descriptor: Option<cubecode::Behavior>,
|
||||
) -> Result<Czyx, crate::SysError> {
|
||||
let coord = self.with_mut(|store| {
|
||||
crate::store_code_cell(store, path, kind, name, links, code, owner)
|
||||
crate::store_code_cell(store, path, kind, name, links, code, owner, descriptor)
|
||||
})?;
|
||||
if let Some(v) = self.get_raw(&coord) {
|
||||
self.log_put(coord, v);
|
||||
|
||||
Reference in New Issue
Block a user