From 26b40a452a920257a146ac2d499d47a3b3a7bb4f Mon Sep 17 00:00:00 2001 From: japabu Date: Thu, 8 Oct 2026 11:39:45 +0200 Subject: [PATCH 1/4] The AML interpreter is walked in declaration order, says what a call took, and takes its wait limit from the caller Slice I1 of the next ACPI stage, host only: what the server's later slices ask of `toyos-aml` before any of them runs a method on a machine. `Interpreter::walk` reads the namespace from the root down, read-only: each object after the one it is in, siblings in the order their tables declared them, an Alias left out, a predefined scope reported as a scope and descended. The order is the arena's, which has been a stack since the meter's change and so is creation order with no number a node: two passes over it make a first-child and a next-sibling link a node, and nothing sorts or recurses. An entry is a depth, a name and a kind; the path of the entry last returned is kept in one string. A caller that kept a path an entry would hold gigabytes for a namespace nested as deep as the meter admits, which a table can choose. The links and the path are taken from the meter while the walk lasts, so a full interpreter refuses to be walked rather than allocate past its bound. `Interpreter::usage` is what the last load or evaluation took: steps, microseconds asked to wait with the refused request included, and the meter's reading at its end. A refusal has one too. `Interpreter::evaluate_within` takes the wait limit; `evaluate` and a load keep ten seconds, now `pub` as `MAX_WAIT_US`. Not here: stores bounded apart from one evaluation's holdings, which is the owner's decision on a full interpreter and a change of its own. Measured on the real machine's tables, outside the tree: 14 of 14 load in 39,419 steps (the DSDT 32,946), the meter at 2,412,886 bytes over 1,819,038 of heap; the walk returns 6,890 entries, 7 deep at most, and held 55,163 bytes; 217 of 267 parents declare their children against name order; the embedded controller is below the PCI root bridge. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A --- ...ine-in-acpi-mode-and-interprets-its-aml.md | 33 ++- userland/acpiserver/aml/src/exec.rs | 14 +- userland/acpiserver/aml/src/lib.rs | 171 +++++++++++-- userland/acpiserver/aml/src/namespace.rs | 104 ++++++++ userland/acpiserver/aml/src/object.rs | 9 +- userland/acpiserver/aml/tests/heap.rs | 28 ++ userland/acpiserver/aml/tests/hostile.rs | 102 ++++++++ userland/acpiserver/aml/tests/walk.rs | 241 ++++++++++++++++++ 8 files changed, 665 insertions(+), 37 deletions(-) create mode 100644 userland/acpiserver/aml/tests/walk.rs diff --git a/issues/toyos-runs-the-machine-in-acpi-mode-and-interprets-its-aml.md b/issues/toyos-runs-the-machine-in-acpi-mode-and-interprets-its-aml.md index 2ba30d45d93..ef0002fc039 100644 --- a/issues/toyos-runs-the-machine-in-acpi-mode-and-interprets-its-aml.md +++ b/issues/toyos-runs-the-machine-in-acpi-mode-and-interprets-its-aml.md @@ -220,18 +220,27 @@ otherwise pay to find again: map leaf of 104 bytes for its entry in its parent and the arena at its doubled capacity: an interpreter filled with field units refuses with 11,111,460 bytes of heap held, and one filled with devices of one child - with 8,900,612. The real machine's tables have not been loaded since the - meter came to count a node's slot, entry and record, a wide field's read - came to hold its buffer and the arena's move to cost steps: that reading - is owed before the server links the crate, and with it the slots the - arena has free after the last table, since a method that defines more - names than that moves the arena each time it runs, at a step for every - 64 bytes of it. Owner: the power-off stage, which gives the server its - memory. **Exit**: the server states its interpreter's bound in heap - bytes, and a host test under a counting allocator holds the most a load - and an evaluation hold while they run to it; and the real machine's - tables load under the meter as it stands, the arena's free slots after - the last read with them. + with 8,900,612. The real machine's tables, loaded outside the tree under + the meter as it stands, with the one word of memory answered and every + function answering as Linux reads it: all 14 load in 39,419 steps, 32,946 of them the + DSDT's, and leave the meter at 2,412,886 bytes over 1,819,038 of heap by + a counting allocator, in 6,891 nodes. Nothing reads the arena's + capacity: by its doubling it is 8,192 slots, 1,301 of them free, and a + method that defines more names than are free moves the arena each time + it runs, at a step for every 64 bytes of it. Owner: the power-off stage, + which gives the server its memory. **Exit**: the server states its + interpreter's bound in heap bytes, and a host test under a counting + allocator holds the most a load and an evaluation hold while they run to + it; and the arena's capacity after the real machine's last table is + read, not derived. +- **The T14's tables are not in name order, and its initialisation does + not hang on the order.** The interpreter's walk of the namespace + (`Interpreter::walk`, `userland/acpiserver/aml/src/lib.rs`) reads + siblings as their tables declared them. Of the 267 objects of the T14's + tables that hold others, 217 declare them against the order their names + sort in; and the embedded controller's device is below the PCI root + bridge, the two whose `_INI` a dry run found writing anything, so a + parent is initialised before its child whichever order siblings take. What the server's load of the tables, on the T14 and through the kernel's mediated access, leaves open: diff --git a/userland/acpiserver/aml/src/exec.rs b/userland/acpiserver/aml/src/exec.rs index 278402fb8fa..e7bbe26753f 100644 --- a/userland/acpiserver/aml/src/exec.rs +++ b/userland/acpiserver/aml/src/exec.rs @@ -33,7 +33,7 @@ use crate::object::{ Object, Ref, Slot, Unresolved, Width, }; use crate::stream::{starts_name, Cursor}; -use crate::{Error, Host, MAX_DEPTH, MAX_NESTING, MAX_STEPS, MAX_WAIT_US, REVISION, WINDOWS, WORK_PER_STEP}; +use crate::{Error, Host, Usage, MAX_DEPTH, MAX_NESTING, MAX_STEPS, REVISION, WINDOWS, WORK_PER_STEP}; pub(crate) struct Machine<'a> { pub(crate) ns: &'a mut Namespace, @@ -43,6 +43,8 @@ pub(crate) struct Machine<'a> { steps: u64, depth: u32, waited_us: u64, + /// The most this load or evaluation may ask to wait. + wait_us: u64, /// Every Mutex acquire not yet released, in order. held: Vec>, /// The SyncLevel of each held Mutex and running Serialized method, in @@ -116,8 +118,8 @@ fn tick(steps: &mut u64) -> Result<(), Error> { } impl<'a> Machine<'a> { - pub(crate) fn new(ns: &'a mut Namespace, host: &'a mut dyn Host, w: Width, meter: Rc) -> Self { - Machine { ns, host, w, meter, steps: 0, depth: 0, waited_us: 0, held: Vec::new(), levels: Vec::new(), global: 0 } + pub(crate) fn new(ns: &'a mut Namespace, host: &'a mut dyn Host, w: Width, meter: Rc, wait_us: u64) -> Self { + Machine { ns, host, w, meter, steps: 0, depth: 0, waited_us: 0, wait_us, held: Vec::new(), levels: Vec::new(), global: 0 } } /// Steps for `bytes` of work done in one: a step a [`WORK_PER_STEP`]. @@ -239,7 +241,9 @@ impl<'a> Machine<'a> { /// Ends an evaluation: whatever it still holds is let go, and holding /// anything at its end is itself a refusal (§19.6.88: "the top-level /// control method cannot exit while still holding ownership of a Mutex"). - pub(crate) fn finish(mut self, r: Result) -> Result { + /// What it took goes to `took`, whichever way it ends. + pub(crate) fn finish(mut self, r: Result, took: &mut Usage) -> Result { + (took.steps, took.waited_us) = (self.steps, self.waited_us); let held = !self.held.is_empty(); for m in self.held.drain(..) { m.held.set(0); @@ -276,7 +280,7 @@ impl<'a> Machine<'a> { fn wait(&mut self, us: u64) -> Result<(), Error> { self.waited_us = self.waited_us.saturating_add(us); - if self.waited_us > MAX_WAIT_US { + if self.waited_us > self.wait_us { return Err(Error::Bound("more time asleep than one evaluation may spend")); } Ok(()) diff --git a/userland/acpiserver/aml/src/lib.rs b/userland/acpiserver/aml/src/lib.rs index b5233010f94..76190c04632 100644 --- a/userland/acpiserver/aml/src/lib.rs +++ b/userland/acpiserver/aml/src/lib.rs @@ -9,7 +9,12 @@ //! what an interpreter holds of tables, namespace and objects is bounded in //! sum, in bytes of heap ([`MAX_LIVE`]). A load refused leaves the namespace //! without anything that table created, and the interpreter holding what it -//! held before. +//! held before. What the last load or evaluation took of those bounds is +//! [`Interpreter::usage`]'s, a refused one's too. +//! +//! [`Interpreter::walk`] reads the namespace an interpreter holds, and runs +//! nothing of it: which objects a caller then evaluates, and in which order, +//! is the caller's. //! //! The library touches no hardware. An operation region's field is read and //! written through the [`Host`] the caller passes, in SystemMemory, @@ -75,8 +80,10 @@ pub const MAX_LIVE: usize = 16 << 20; /// The bytes of work one step stands for: a step for every this many bytes /// an operation makes, copies, compares or walks. pub(crate) const WORK_PER_STEP: usize = 64; -/// The time one evaluation may ask to Sleep, Stall and Wait, together, in µs. -pub(crate) const MAX_WAIT_US: u64 = 10_000_000; +/// The time a load, and an evaluation whose caller names no other limit, may +/// ask to Sleep, Stall and Wait, together, in µs: time asked for, whatever +/// the host then spends. +pub const MAX_WAIT_US: u64 = 10_000_000; /// What the Revision opcode answers (§19.6.119): this interpreter's revision. pub(crate) const REVISION: u64 = 1; @@ -193,12 +200,105 @@ pub enum Value { Reference(String), } +/// What a load or an evaluation took of its bounds, a refused one as well. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct Usage { + /// The steps it took: terms, arguments, loop iterations, field access + /// units, and the bytes it made, copied, compared or walked. + pub steps: u64, + /// The time it asked to Sleep, Stall and Wait, in µs, the request its + /// limit refused included. + pub waited_us: u64, + /// The heap the interpreter held at its end, of [`MAX_LIVE`]. + pub live: usize, +} + +/// What a named object is: the types of Table 19.36, a predefined scope, +/// which has none, and a reference a Name holds. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Kind { + Uninitialized, + Integer, + String, + Buffer, + Package, + FieldUnit, + Device, + Event, + Method, + Mutex, + OperationRegion, + PowerResource, + /// The Processor object ACPI 6.4 deprecated (ACPI 6.3A §19.6.108). + Processor, + ThermalZone, + BufferField, + /// `\_SB` and the other scopes of §5.3.1: descended, and no device. + Scope, + Reference, +} + +/// One object of a [`Walk`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Entry { + /// How many names its path has: 1 directly below the root. Everything + /// below an object follows it at a greater depth, so a caller leaves a + /// subtree out by skipping to the next entry no deeper than its top. + pub depth: u32, + pub name: [u8; 4], + pub kind: Kind, +} + +/// The namespace read from the root down ([`Interpreter::walk`]). A table +/// chooses how deep it nests, and a path is as long as its depth: an entry +/// is as small at any depth, and is what a caller keeps of a whole +/// namespace, where the paths of one nested as deep as [`MAX_LIVE`] admits +/// are gigabytes. +pub struct Walk<'a>(namespace::Walk<'a>); + +impl Walk<'_> { + /// The absolute path of the entry last returned, as + /// [`Interpreter::evaluate`] takes it. + pub fn path(&self) -> &str { + self.0.path() + } +} + +impl Iterator for Walk<'_> { + type Item = Entry; + + fn next(&mut self) -> Option { + let (depth, seg, object) = self.0.step()?; + let kind = match object { + Object::Uninit => Kind::Uninitialized, + Object::Int(_) => Kind::Integer, + Object::Str(_) => Kind::String, + Object::Buf(_) => Kind::Buffer, + Object::Pkg(_) => Kind::Package, + Object::Field(_) => Kind::FieldUnit, + Object::BufField(_) => Kind::BufferField, + Object::Ref(_) | Object::Lazy(_) => Kind::Reference, + Object::Scope => Kind::Scope, + Object::Device => Kind::Device, + Object::Processor => Kind::Processor, + Object::ThermalZone => Kind::ThermalZone, + Object::PowerResource => Kind::PowerResource, + Object::Method(_) => Kind::Method, + Object::Mutex(_) => Kind::Mutex, + Object::Event(_) => Kind::Event, + Object::Region(_) => Kind::OperationRegion, + }; + Some(Entry { depth, name: seg.0, kind }) + } +} + /// One machine's namespace, and what loaded it. pub struct Interpreter { ns: Namespace, meter: Rc, /// Set by the DSDT's revision (§19.6.29), for every table after it. width: Option, + last: Usage, } impl Default for Interpreter { @@ -228,12 +328,20 @@ impl Interpreter { let os = meter.bytes(b"Microsoft Windows NT".to_vec()).expect(held); put(b"_OS_", Object::Str(os)); put(b"_REV", Object::Int(2)); - Interpreter { ns, meter, width: None } + let last = Usage { live: meter.live(), ..Usage::default() }; + Interpreter { ns, meter, width: None, last } } /// Loads a DSDT or SSDT (§5.4.2): the DSDT first, then each SSDT. The /// table's header, length and checksum are [`toyos_acpi::Table::open`]'s. pub fn load(&mut self, host: &mut dyn Host, table: &toyos_acpi::Table

) -> Result<(), Error> { + self.last = Usage::default(); + let r = self.load_in(host, table); + self.last.live = self.meter.live(); + r + } + + fn load_in(&mut self, host: &mut dyn Host, table: &toyos_acpi::Table

) -> Result<(), Error> { let bytes: Vec = (0..table.len()).map_while(|i| table.byte(i)).collect(); let w = match (&bytes[..4], self.width) { (b"DSDT", None) => Width { bits: if bytes[toyos_acpi::SDT_REVISION] < 2 { 32 } else { 64 } }, @@ -246,39 +354,66 @@ impl Interpreter { let table = self.meter.bytes(bytes)?; let (root, made) = (self.ns.root(), self.ns.mark()); let mut f = Frame::new(root, Vec::new(), table.clone(), 0); - let mut m = Machine::new(&mut self.ns, host, w, self.meter.clone()); + let mut m = Machine::new(&mut self.ns, host, w, self.meter.clone(), MAX_WAIT_US); let bytes = table.borrow(); let mut c = stream::Cursor::new(&bytes, toyos_acpi::SDT_HEADER_LEN, bytes.len()); let r = m.term_list(&mut f, &mut c).and_then(|flow| match flow { exec::Flow::Next => Ok(()), _ => Err(Error::Rule("a Return, Break or Continue at definition block level")), }); - let r = m.finish(r); - match r { - Ok(()) => { - self.width = Some(w); - Ok(()) - } - Err(e) => { - self.ns.unwind(made); - Err(e) - } + let r = m.finish(r, &mut self.last); + if r.is_ok() { + self.width = Some(w); + } else { + self.ns.unwind(made); } + r } /// Evaluates the object at an absolute path, written `\_SB.PCI0._STA`: a - /// method is invoked with `args`, anything else is its value. + /// method is invoked with `args`, anything else is its value. It may ask + /// to wait [`MAX_WAIT_US`]. pub fn evaluate(&mut self, host: &mut dyn Host, path: &str, args: &[Value]) -> Result { + self.evaluate_within(host, path, args, MAX_WAIT_US) + } + + /// [`Interpreter::evaluate`], asking to Sleep, Stall and Wait `wait_us` + /// at most, which the caller names for a method it knows to ask more + /// than [`MAX_WAIT_US`]. + pub fn evaluate_within(&mut self, host: &mut dyn Host, path: &str, args: &[Value], wait_us: u64) -> Result { + self.last = Usage::default(); + let r = self.evaluate_in(host, path, args, wait_us); + self.last.live = self.meter.live(); + r + } + + fn evaluate_in(&mut self, host: &mut dyn Host, path: &str, args: &[Value], wait_us: u64) -> Result { let w = self.width.ok_or(Error::Table("nothing is loaded"))?; let id = self.named(path)?; let args = args.iter().map(|a| self.object_of(a, w, 0)).collect::, _>>()?; let meter = self.meter.clone(); - let mut m = Machine::new(&mut self.ns, host, w, meter.clone()); + let mut m = Machine::new(&mut self.ns, host, w, meter.clone(), wait_us); let mut handed = 0; let r = m.evaluate(id, args).and_then(|o| value_of(&mut m, &meter, &mut handed, o, 0)); // The value is the caller's from here, and no longer this interpreter's. meter.give(handed); - m.finish(r) + m.finish(r, &mut self.last) + } + + /// What the last load or evaluation took, and what the interpreter held + /// at its end. + pub fn usage(&self) -> Usage { + self.last + } + + /// Every object the loaded tables and this interpreter defined, read-only + /// and without evaluating any: each after the object it is in, siblings + /// in the order they were declared, a table's after those of the tables + /// loaded before it. An Alias is not among them; what it names is, where + /// it was defined. The walk is held against [`MAX_LIVE`] while it lasts, + /// and refused where that has no room for it. + pub fn walk(&self) -> Result, Error> { + self.ns.walk().map(Walk) } fn object_of(&self, v: &Value, w: Width, depth: usize) -> Result { diff --git a/userland/acpiserver/aml/src/namespace.rs b/userland/acpiserver/aml/src/namespace.rs index a4c12976317..cf888b4c159 100644 --- a/userland/acpiserver/aml/src/namespace.rs +++ b/userland/acpiserver/aml/src/namespace.rs @@ -17,6 +17,10 @@ //! and a parent whose last child it took no map. The arena therefore moves //! as often as a method that grows it runs, and a move pays the [`Toll`] for //! the bytes it copies. +//! +//! The arena's order is the order its nodes were declared in, and a [`Walk`] +//! reads sibling order from it alone: a parent's map keeps its children by +//! name. use alloc::collections::BTreeMap; use alloc::rc::Rc; @@ -241,3 +245,103 @@ impl Namespace { Ok(crate::name::text(&Path { root: true, up: 0, segs })) } } + +/// Every object below the root, each after its parent and siblings in the +/// order they were declared, an Alias left out: its source is met at its own +/// place, and an Alias of an ancestor would otherwise never end. +/// +/// It is held against the meter while it lasts, at eight bytes a node and +/// five a level of the deepest: both are as many as the tables chose. +pub(crate) struct Walk<'a> { + ns: &'a Namespace, + /// A node's first child and its next sibling, by index; 0, the root's, + /// is none. + first: Vec, + next: Vec, + at: u32, + depth: u32, + path: String, + held: usize, +} + +/// The bytes of a path `depth` names long, `\AAAA.BBBB`. +const fn path_len(depth: usize) -> usize { + 5 * depth +} + +impl Namespace { + pub(crate) fn walk(&self) -> Result, Error> { + let count = self.nodes.len(); + let links = 2 * count * core::mem::size_of::(); + self.meter.take(links)?; + let mut walk = Walk { ns: self, first: Vec::new(), next: Vec::new(), at: 0, depth: 1, path: String::new(), held: links }; + for links in [&mut walk.first, &mut walk.next] { + links.reserve_exact(count); + links.resize(count, 0); + } + // A parent is older than its child, so its depth is known first: + // `next` holds each node's depth until the links are made. + let mut deepest = 0; + for (i, n) in self.nodes.iter().enumerate() { + if let Some(p) = n.parent { + walk.next[i] = walk.next[p as usize] + 1; + deepest = deepest.max(walk.next[i]); + } + } + let path = path_len(deepest as usize); + self.meter.take(path)?; + walk.held += path; + walk.path.reserve_exact(path); + // Newest first, each node goes to the front of its parent's list, + // which leaves the oldest there. + for (i, n) in self.nodes.iter().enumerate().rev() { + walk.next[i] = 0; + if let (Some(p), None) = (n.parent, n.alias) { + walk.next[i] = walk.first[p as usize]; + walk.first[p as usize] = i as u32; + } + } + walk.at = walk.first[0]; + Ok(walk) + } +} + +impl Drop for Walk<'_> { + fn drop(&mut self) { + self.ns.meter.give(self.held); + } +} + +impl Walk<'_> { + /// The next object's depth, a child of the root's being 1, its name and + /// the object. + pub(crate) fn step(&mut self) -> Option<(u32, Seg, &Object)> { + let i = self.at as usize; + if i == 0 { + return None; + } + let (n, depth) = (&self.ns.nodes[i], self.depth); + self.path.truncate(path_len(depth as usize - 1)); + self.path.push(if depth == 1 { '\\' } else { '.' }); + self.path.extend(n.seg.0.iter().map(|&c| char::from(c))); + if self.first[i] != 0 { + self.at = self.first[i]; + self.depth += 1; + } else { + // The next sibling of this node or of the nearest ancestor that + // has one: each node is climbed from once in a whole walk. + let mut up = i; + while up != 0 && self.next[up] == 0 { + up = self.ns.nodes[up].parent.map_or(0, |p| p as usize); + self.depth -= 1; + } + self.at = self.next[up]; + } + Some((depth, n.seg, &n.object)) + } + + /// The absolute path of the object last stepped to. + pub(crate) fn path(&self) -> &str { + &self.path + } +} diff --git a/userland/acpiserver/aml/src/object.rs b/userland/acpiserver/aml/src/object.rs index aef674790a2..c81041391b5 100644 --- a/userland/acpiserver/aml/src/object.rs +++ b/userland/acpiserver/aml/src/object.rs @@ -39,8 +39,9 @@ pub(crate) type Kept = Rc>; /// for: a string, buffer, table or package at its vector's capacity and its /// shared record, a fixed record at its size, the namespace's arena at its /// capacity and a node's entry among its parent's children at the most a -/// parent allocates for one (`namespace`), and the value an evaluation is -/// building for its caller, until the caller has it. What it does not count +/// parent allocates for one (`namespace`), the value an evaluation is +/// building for its caller, until the caller has it, and a walk of the +/// namespace while its caller holds it. What it does not count /// is what a load or an evaluation holds while it runs and lets go at its /// end: its frames and the mutexes it has acquired, the names it is /// reading, the bytes one operator has made and not yet held (`exec`) and @@ -69,6 +70,10 @@ impl Meter { Ok(()) } + pub(crate) fn live(&self) -> usize { + self.live.get() + } + pub(crate) fn give(&self, n: usize) { self.live.set(self.live.get().checked_sub(n).expect("the meter gives back only what it took")); } diff --git a/userland/acpiserver/aml/tests/heap.rs b/userland/acpiserver/aml/tests/heap.rs index c7cf8226281..f4ad8696f8d 100644 --- a/userland/acpiserver/aml/tests/heap.rs +++ b/userland/acpiserver/aml/tests/heap.rs @@ -240,6 +240,34 @@ fn a_parent_whose_last_child_is_unwound_holds_no_map() { assert_eq!(kept, held, "bytes held after the two evaluations, and before them"); } +/// A walk of 40,000 devices holds a third of a mebibyte, which is the +/// interpreter's while the walk lasts: one that is full refuses the walk, +/// and one that is not holds afterwards what it held before. +#[test] +fn a_walk_is_held_against_the_bound_while_it_lasts() { + let devices: Vec> = (0..40_000).map(|d| device(&seg(d), &[])).collect(); + let (fill, fills) = fillers(8, &buffer(&int(0), &[])); + let (mut i, before) = start(&fill); + i.load_bytes(&mut Sink, &table(b"SSDT", 2, &devices.concat())).expect("the table loads"); + let live = i.usage().live; + let held = heap() - before; + let walk = i.walk().expect("an interpreter with room is walked"); + // The root is no entry, and has its links as every node has. + let nodes = walk.count() as isize + 1; + assert!(nodes > 40_000, "{nodes} nodes"); + let walking = PEAK.get() - before - held; + // Two links a node, and a path one name long. + assert_eq!(walking, 8 * nodes + 5, "bytes a walk of {nodes} nodes held"); + assert_eq!(HELD.get() - before, held, "bytes held after the walk, and before it"); + assert_eq!(i.evaluate(&mut Sink, "\\_REV", &[]), Ok(Value::Integer(2))); + assert_eq!(i.usage().live, live); + + let refused = fills.iter().find_map(|m| i.evaluate(&mut Sink, m, &[]).err()); + Filled::of(before, refused).within_the_bound(); + assert_eq!(i.walk().err(), Some(Error::Bound(FULL))); + assert_eq!(i.evaluate(&mut Sink, "\\_REV", &[]), Ok(Value::Integer(2))); +} + /// One table naming 204,000 field units is refused, and the interpreter /// then holds what it held before, to the byte: its arena has the slots it /// had, and what it had room for it has room for again. diff --git a/userland/acpiserver/aml/tests/hostile.rs b/userland/acpiserver/aml/tests/hostile.rs index 5f5ff0f09f6..fd98781e840 100644 --- a/userland/acpiserver/aml/tests/hostile.rs +++ b/userland/acpiserver/aml/tests/hostile.rs @@ -524,6 +524,108 @@ fn waits_and_stalls_are_bounded_in_time_asked() { assert!(stalled <= 10_000_000, "{stalled} µs"); } +fn sleep(ms: u64) -> Vec { + cat(&[&[0x5B, 0x22], &int(ms)]) +} + +fn stall(us: u64) -> Vec { + cat(&[&[0x5B, 0x21], &int(us)]) +} + +/// The limit is the most an evaluation may ask, to the microsecond: the +/// caller's where it names one, and ten seconds where it does not, for a +/// load too. What crosses it is not asked of the host. +#[test] +fn the_wait_limit_is_exact_and_the_callers_to_name() { + let (mut i, mut m) = loaded(&cat(&[ + &method("TEN0", 0, &cat(&[&sleep(9_999), &stall(250), &stall(250), &stall(250), &stall(250), &ret(&int(1))])), + &method("TEN1", 0, &cat(&[&sleep(10_000), &stall(1), &ret(&int(1))])), + &method("TWEN", 0, &cat(&[&sleep(20_000), &ret(&int(2))])), + &method("TWE1", 0, &cat(&[&sleep(20_000), &stall(1), &ret(&int(2))])), + &method("NONE", 0, &cat(&[&sleep(0), &stall(0), &ret(&int(3))])), + &method("ONE0", 0, &cat(&[&stall(1), &ret(&int(3))])), + ])); + let asleep = Error::Bound("more time asleep than one evaluation may spend"); + let over = Err(asleep.clone()); + assert_eq!(toyos_aml::MAX_WAIT_US, 10_000_000); + + assert_eq!(i.evaluate(&mut m, "\\TEN0", &[]), Ok(Value::Integer(1))); + assert_eq!(i.usage().waited_us, 10_000_000); + m.log.clear(); + assert_eq!(i.evaluate(&mut m, "\\TEN1", &[]), over); + assert_eq!(i.usage().waited_us, 10_000_001); + assert_eq!(m.log, vec![Event::Sleep(10_000)]); + assert_eq!(i.evaluate(&mut m, "\\TWEN", &[]), over); + + assert_eq!(i.evaluate_within(&mut m, "\\TWEN", &[], 20_000_000), Ok(Value::Integer(2))); + assert_eq!(i.usage().waited_us, 20_000_000); + assert_eq!(i.evaluate_within(&mut m, "\\TWE1", &[], 20_000_000), over); + assert_eq!(i.evaluate_within(&mut m, "\\TEN0", &[], 9_999_999), over); + assert_eq!(i.evaluate_within(&mut m, "\\NONE", &[], 0), Ok(Value::Integer(3))); + assert_eq!(i.evaluate_within(&mut m, "\\ONE0", &[], 0), over); + + let mut m = Machine::default(); + assert_eq!(Interpreter::new().load_bytes(&mut m, &dsdt(&cat(&[&sleep(10_000), &def_name("A", &int(1))]))), Ok(())); + assert_eq!(Interpreter::new().load_bytes(&mut m, &dsdt(&cat(&[&sleep(10_000), &stall(1)]))), Err(asleep)); +} + +/// What a load or an evaluation took is read after it, a refused one's too, +/// and is that call's alone. +#[test] +fn usage_is_each_calls_own_and_a_refusals_too() { + let mut m = Machine::default(); + let mut i = Interpreter::new(); + let empty = i.usage(); + assert_eq!((empty.steps, empty.waited_us), (0, 0)); + assert!(empty.live > 0 && empty.live < 4096, "{empty:?}"); + + let body = cat(&[ + &def_name("PKG0", &package(&[int(0)])), + &method("SPIN", 0, &while_(&int(1), &[0xA3])), + &method("NAP0", 0, &cat(&[&sleep(3), &stall(5)])), + &method("KEEP", 0, &store(&buffer(&int(1000), &[]), &index(&name("PKG0"), &int(0), ZERO))), + &method("DROP", 0, &store(&int(0), &index(&name("PKG0"), &int(0), ZERO))), + &method("RETB", 0, &ret(&buffer(&int(1000), &[]))), + ]); + i.load_bytes(&mut m, &dsdt(&body)).unwrap(); + let loaded = i.usage(); + // Six terms and what they hold: a load is counted as an evaluation is. + assert!(loaded.steps >= 6 && loaded.steps < 64, "{loaded:?}"); + assert_eq!(loaded.waited_us, 0); + assert!(loaded.live > empty.live + body.len(), "{loaded:?}"); + + // The step bound's refusal has taken the bound and the step it refused. + assert!(matches!(i.evaluate(&mut m, "\\SPIN", &[]), Err(Error::Bound(_)))); + assert_eq!(i.usage(), toyos_aml::Usage { steps: (1 << 20) + 1, waited_us: 0, live: loaded.live }); + + i.evaluate(&mut m, "\\NAP0", &[]).unwrap(); + let napped = i.usage(); + assert_eq!((napped.waited_us, napped.live), (3_005, loaded.live)); + assert!(napped.steps >= 4 && napped.steps < 16, "{napped:?}"); + + // What an evaluation leaves stored is held after it; what it hands its + // caller is not. + i.evaluate(&mut m, "\\KEEP", &[]).unwrap(); + let kept = i.usage(); + assert!(kept.live >= loaded.live + 1000 && kept.live < loaded.live + 1100, "{kept:?}"); + assert_eq!(kept.waited_us, 0); + assert_eq!(i.evaluate(&mut m, "\\RETB", &[]), Ok(Value::Buffer(vec![0; 1000]))); + assert_eq!(i.usage().live, kept.live); + i.evaluate(&mut m, "\\DROP", &[]).unwrap(); + assert_eq!(i.usage().live, loaded.live); + + // A refusal before anything ran took nothing. + assert!(matches!(i.evaluate(&mut m, "\\NONE", &[]), Err(Error::NotFound(_)))); + assert_eq!(i.usage(), toyos_aml::Usage { steps: 0, waited_us: 0, live: loaded.live }); + + // A refused load took its steps, and holds what was held before it. + let refused = table(b"SSDT", 2, &cat(&[&def_name("\\NEW0", &buffer(&int(5000), &[])), &sleep(7), &def_name("\\PKG0", &int(1))])); + assert!(matches!(i.load_bytes(&mut m, &refused), Err(Error::Exists(_)))); + let after = i.usage(); + assert_eq!((after.waited_us, after.live), (7_000, loaded.live)); + assert!(after.steps >= 3 && after.steps < 256, "{after:?}"); +} + /// BLOCKER 9 (c): a reference to an object a method created names nothing /// once the method exits, even after its slot is reused. #[test] diff --git a/userland/acpiserver/aml/tests/walk.rs b/userland/acpiserver/aml/tests/walk.rs new file mode 100644 index 00000000000..3c46eb9ab4f --- /dev/null +++ b/userland/acpiserver/aml/tests/walk.rs @@ -0,0 +1,241 @@ +//! The namespace read from the root down (§5.3): every object once, after +//! the object it is in, siblings in the order their tables declared them, +//! whatever a table's names sort as, however deep it nests, and whatever a +//! refused load or a method's exit took out of the arena before. + +mod common; + +use common::*; +use toyos_aml::{Error, Interpreter, Kind, Value}; + +/// What §5.3.1 and §5.7 predefine, in the order `Interpreter::new` makes them. +const PREDEFINED: [(&str, Kind); 9] = [ + ("\\_GPE", Kind::Scope), + ("\\_PR_", Kind::Scope), + ("\\_SB_", Kind::Scope), + ("\\_SI_", Kind::Scope), + ("\\_TZ_", Kind::Scope), + ("\\_GL_", Kind::Mutex), + ("\\_OSI", Kind::Method), + ("\\_OS_", Kind::String), + ("\\_REV", Kind::Integer), +]; + +/// Every entry's path and kind, after the predefined ones, each checked +/// against its depth and its name. +fn walked(i: &Interpreter) -> Vec<(String, Kind)> { + let mut w = i.walk().unwrap(); + let mut out = Vec::new(); + while let Some(e) = w.next() { + let path = w.path(); + assert_eq!(path.len(), 5 * e.depth as usize, "{path}"); + assert_eq!(&path.as_bytes()[path.len() - 4..], &e.name, "{path}"); + out.push((path.to_string(), e.kind)); + } + let predefined: Vec<(String, Kind)> = PREDEFINED.iter().map(|&(p, k)| (p.to_string(), k)).collect(); + assert_eq!(out[..PREDEFINED.len()], predefined); + out.split_off(PREDEFINED.len()) +} + +fn listed(entries: &[(&str, Kind)]) -> Vec<(String, Kind)> { + entries.iter().map(|&(p, k)| (p.to_string(), k)).collect() +} + +fn alias(source: &str, name_: &str) -> Vec { + cat(&[&[0x06], &name(source), &name(name_)]) +} + +/// Names that sort against the order they are declared in, at the root and +/// below it; a second table that adds to a device of the first; a table +/// refused after it made objects, and a method that made some and exited, +/// both before the table whose objects take their slots. +#[test] +fn siblings_are_read_in_the_order_they_were_declared() { + let (mut i, mut m) = loaded(&cat(&[ + &device("ZZZZ", &cat(&[&def_name("YYYY", &int(1)), &def_name("BBBB", &int(2))])), + &device("MMMM", &[]), + &method("MAKE", 0, &cat(&[&device("\\ZZZZ.TMP0", &[]), &def_name("\\AAA0", &int(0))])), + ])); + let before = walked(&i); + assert_eq!( + before, + listed(&[ + ("\\ZZZZ", Kind::Device), + ("\\ZZZZ.YYYY", Kind::Integer), + ("\\ZZZZ.BBBB", Kind::Integer), + ("\\MMMM", Kind::Device), + ("\\MAKE", Kind::Method), + ]) + ); + + // A table that defines three objects and then collides: all three go. + let refused = table( + b"SSDT", + 2, + &cat(&[&device("\\ZZZZ.GONE", &def_name("X", &int(1))), &def_name("\\AAAA", &int(1)), &def_name("\\MMMM", &int(1))]), + ); + assert_eq!(i.load_bytes(&mut m, &refused), Err(Error::Exists("\\MMMM".into()))); + assert_eq!(walked(&i), before); + assert_eq!(i.evaluate(&mut m, "\\MAKE", &[]), Ok(Value::Uninitialized)); + assert_eq!(walked(&i), before); + + let ssdt = table( + b"SSDT", + 2, + &cat(&[ + &def_name("\\ZZZZ.CCCC", &int(3)), + &def_name("\\AAAA", &int(4)), + &scope("\\ZZZZ", &def_name("AAAA", &int(5))), + &def_name("\\MMMM.KKKK", &int(6)), + ]), + ); + i.load_bytes(&mut m, &ssdt).unwrap(); + let after = walked(&i); + assert_eq!( + after, + listed(&[ + ("\\ZZZZ", Kind::Device), + ("\\ZZZZ.YYYY", Kind::Integer), + ("\\ZZZZ.BBBB", Kind::Integer), + ("\\ZZZZ.CCCC", Kind::Integer), + ("\\ZZZZ.AAAA", Kind::Integer), + ("\\MMMM", Kind::Device), + ("\\MMMM.KKKK", Kind::Integer), + ("\\MAKE", Kind::Method), + ("\\AAAA", Kind::Integer), + ]) + ); + // A walked path is one `evaluate` takes. + let values: Vec = + after.iter().filter(|(_, k)| *k == Kind::Integer).map(|(p, _)| i.evaluate(&mut m, p, &[]).unwrap()).collect(); + assert_eq!(values, [1, 2, 3, 5, 6, 4].map(Value::Integer)); +} + +/// An Alias is no second object: not of a device, whose `_INI` would run +/// twice, and not of an ancestor, below which a walk that followed it would +/// find itself again. +#[test] +fn an_alias_is_not_walked() { + let (mut i, mut m) = loaded(&cat(&[ + &device( + "DEV0", + &cat(&[&def_name("_INI", &int(7)), &device("SUB0", &alias("\\DEV0", "LOOP")), &alias("SUB0", "TWIN")]), + ), + &alias("\\DEV0", "ALI0"), + ])); + assert_eq!( + walked(&i), + listed(&[("\\DEV0", Kind::Device), ("\\DEV0._INI", Kind::Integer), ("\\DEV0.SUB0", Kind::Device)]) + ); + // The aliases are there, and name what the walk met once. + assert_eq!(i.evaluate(&mut m, "\\ALI0._INI", &[]), Ok(Value::Integer(7))); + assert_eq!(i.evaluate(&mut m, "\\DEV0.SUB0.LOOP.TWIN.LOOP._INI", &[]), Ok(Value::Integer(7))); +} + +/// §5.3.1's scopes are descended and are no device; every other type is +/// what Table 19.36 calls it. +#[test] +fn every_object_is_walked_as_what_it_is() { + let processor = cat(&[&[0x5B, 0x83], &pkg(&cat(&[&name("CPU0"), &[0x01, 0x10, 0x04, 0x00, 0x00, 0x06]]))]); + let (i, _) = loaded(&cat(&[ + &scope("\\_SB", &device("PCI0", &method("_INI", 0, &[]))), + &scope("\\_PR", &processor), + &thermal_zone("\\_TZ.TZ00", &[]), + &power_resource("PWR0", &[]), + &def_name("STR0", &string("s")), + &def_name("BUF0", &buffer(&int(4), &[])), + &def_name("PKG0", &package(&[int(1)])), + &op_region("REG0", 0x00, &int(0), &int(8)), + &field("REG0", 0x01, &[unit("FLD0", 8)]), + &cat(&[&[0x8C], &name("BUF0"), &int(0), &name("BFL0")]), + &cat(&[&[0x5B, 0x01], &name("MTX0"), &[0x00]]), + &cat(&[&[0x5B, 0x02], &name("EVT0")]), + &def_name("REF0", &int(0)), + &method("MKRF", 0, ©_object(&ref_of(&name("STR0")), &name("REF0"))), + ])); + let mut i = i; + i.evaluate(&mut Machine::default(), "\\MKRF", &[]).unwrap(); + let mut w = i.walk().unwrap(); + let mut seen = Vec::new(); + while let Some(e) = w.next() { + seen.push((w.path().to_string(), e.depth, e.kind)); + } + let at = |path: &str| { + let (_, depth, kind) = seen.iter().find(|(p, ..)| p == path).unwrap_or_else(|| panic!("{path} is not walked")); + (*depth, *kind) + }; + assert_eq!(at("\\_SB_"), (1, Kind::Scope)); + assert_eq!(at("\\_SB_.PCI0"), (2, Kind::Device)); + assert_eq!(at("\\_SB_.PCI0._INI"), (3, Kind::Method)); + assert_eq!(at("\\_PR_.CPU0"), (2, Kind::Processor)); + assert_eq!(at("\\_TZ_.TZ00"), (2, Kind::ThermalZone)); + assert_eq!(at("\\PWR0"), (1, Kind::PowerResource)); + assert_eq!(at("\\STR0"), (1, Kind::String)); + assert_eq!(at("\\BUF0"), (1, Kind::Buffer)); + assert_eq!(at("\\PKG0"), (1, Kind::Package)); + assert_eq!(at("\\REG0"), (1, Kind::OperationRegion)); + assert_eq!(at("\\FLD0"), (1, Kind::FieldUnit)); + assert_eq!(at("\\BFL0"), (1, Kind::BufferField)); + assert_eq!(at("\\MTX0"), (1, Kind::Mutex)); + assert_eq!(at("\\EVT0"), (1, Kind::Event)); + assert_eq!(at("\\REF0"), (1, Kind::Reference)); + // What a scope holds follows it, before the scope declared after it. + let order: Vec<&str> = seen.iter().map(|(p, ..)| p.as_str()).filter(|p| p.starts_with("\\_")).collect(); + assert_eq!( + order, + ["\\_GPE", "\\_PR_", "\\_PR_.CPU0", "\\_SB_", "\\_SB_.PCI0", "\\_SB_.PCI0._INI", "\\_SI_", "\\_TZ_", "\\_TZ_.TZ00", "\\_GL_", "\\_OSI", "\\_OS_", "\\_REV"] + ); +} + +/// A device in a device, `levels` deep, below the `above` that are there: +/// the devices are reached through Scopes of at most 200 names each, so a +/// table nests no deeper in terms than the interpreter goes. +fn deeper(above: usize, levels: usize) -> Vec { + let mut body = Vec::new(); + for _ in 0..levels { + body = device("D", &body); + } + let mut left = above; + while left > 0 { + let chunk = if left % 200 == 0 { 200 } else { left % 200 }; + left -= chunk; + let path = vec!["D"; chunk].join("."); + body = scope(&if left == 0 { format!("\\{path}") } else { path }, &body); + } + body +} + +/// A table nests its namespace as deep as it likes, far past what a walk +/// that recursed, or one that kept a path an entry, could follow. +#[test] +fn a_namespace_nested_deep_is_walked_whole() { + const TABLES: usize = 120; + const LEVELS: usize = 100; + let mut m = Machine::default(); + let mut i = Interpreter::new(); + i.load_bytes(&mut m, &dsdt(&deeper(0, LEVELS))).unwrap(); + for t in 1..TABLES { + i.load_bytes(&mut m, &table(b"SSDT", 2, &deeper(t * LEVELS, LEVELS))).unwrap(); + } + let mut w = i.walk().unwrap(); + let mut depths = Vec::new(); + while let Some(e) = w.next() { + if e.name == *b"D___" { + assert_eq!(e.kind, Kind::Device); + assert_eq!(w.path().len(), 5 * e.depth as usize); + depths.push(e.depth as usize); + } + } + assert_eq!(depths, (1..=TABLES * LEVELS).collect::>()); +} + +/// A parent of thousands, declared against the order their names sort in. +#[test] +fn a_parent_of_thousands_is_walked_in_their_order() { + let names: Vec = (0..4000u32).rev().map(|n| format!("{:04X}", 0xA000 + n)).collect(); + let units: Vec> = names.iter().map(|n| unit(n, 8)).collect(); + let (i, _) = loaded(&cat(&[&op_region("MEM", 0x00, &int(0), &int(0x1000)), &device("WIDE", &field("\\MEM", 0x01, &units))])); + let got: Vec = walked(&i).into_iter().filter(|(_, k)| *k == Kind::FieldUnit).map(|(p, _)| p).collect(); + let want: Vec = names.iter().map(|n| format!("\\WIDE.{n}")).collect(); + assert_eq!(got, want); +} From 885d8d32758ede1d4e6e477d67bacd66ff0c7b38 Mon Sep 17 00:00:00 2001 From: japabu Date: Thu, 8 Oct 2026 12:52:36 +0200 Subject: [PATCH 2/4] The caller's wait limit goes, and a walk's charge is held to the byte Review round 1 of #766. `Interpreter::evaluate_within`, `Machine::wait_us` and the `pub` on `MAX_WAIT_US` are deleted: nothing called them, and no reading shows a method that asks for more than ten seconds. The limit is the constant again, for a load and an evaluation alike, and the track issue records which slice brings a caller's limit back and on what reading. The wait test keeps the exact-at-ten-seconds and load cases. `a_walk_takes_its_links_and_its_deepest_path_from_the_bound` leaves a namespace nested 12,000 deep exactly the bytes its walk takes, eight a node and five a level, and then one byte less: the first is walked, the second refused with the links it had taken given back. Nothing before it could fail on the path's charge, on the refusal at it, or on the links being charged as one vector's. `usage_is_each_calls_own_and_a_refusals_too` now reads a load refused before anything ran, a second DSDT after a load that took steps and time: zero of both, which holds `load`'s reset. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A --- userland/acpiserver/aml/src/exec.rs | 10 ++-- userland/acpiserver/aml/src/lib.rs | 24 +++------- userland/acpiserver/aml/tests/hostile.rs | 23 +++------- userland/acpiserver/aml/tests/walk.rs | 58 +++++++++++++++++++++++- 4 files changed, 74 insertions(+), 41 deletions(-) diff --git a/userland/acpiserver/aml/src/exec.rs b/userland/acpiserver/aml/src/exec.rs index e7bbe26753f..20143bcb625 100644 --- a/userland/acpiserver/aml/src/exec.rs +++ b/userland/acpiserver/aml/src/exec.rs @@ -33,7 +33,7 @@ use crate::object::{ Object, Ref, Slot, Unresolved, Width, }; use crate::stream::{starts_name, Cursor}; -use crate::{Error, Host, Usage, MAX_DEPTH, MAX_NESTING, MAX_STEPS, REVISION, WINDOWS, WORK_PER_STEP}; +use crate::{Error, Host, Usage, MAX_DEPTH, MAX_NESTING, MAX_STEPS, MAX_WAIT_US, REVISION, WINDOWS, WORK_PER_STEP}; pub(crate) struct Machine<'a> { pub(crate) ns: &'a mut Namespace, @@ -43,8 +43,6 @@ pub(crate) struct Machine<'a> { steps: u64, depth: u32, waited_us: u64, - /// The most this load or evaluation may ask to wait. - wait_us: u64, /// Every Mutex acquire not yet released, in order. held: Vec>, /// The SyncLevel of each held Mutex and running Serialized method, in @@ -118,8 +116,8 @@ fn tick(steps: &mut u64) -> Result<(), Error> { } impl<'a> Machine<'a> { - pub(crate) fn new(ns: &'a mut Namespace, host: &'a mut dyn Host, w: Width, meter: Rc, wait_us: u64) -> Self { - Machine { ns, host, w, meter, steps: 0, depth: 0, waited_us: 0, wait_us, held: Vec::new(), levels: Vec::new(), global: 0 } + pub(crate) fn new(ns: &'a mut Namespace, host: &'a mut dyn Host, w: Width, meter: Rc) -> Self { + Machine { ns, host, w, meter, steps: 0, depth: 0, waited_us: 0, held: Vec::new(), levels: Vec::new(), global: 0 } } /// Steps for `bytes` of work done in one: a step a [`WORK_PER_STEP`]. @@ -280,7 +278,7 @@ impl<'a> Machine<'a> { fn wait(&mut self, us: u64) -> Result<(), Error> { self.waited_us = self.waited_us.saturating_add(us); - if self.waited_us > self.wait_us { + if self.waited_us > MAX_WAIT_US { return Err(Error::Bound("more time asleep than one evaluation may spend")); } Ok(()) diff --git a/userland/acpiserver/aml/src/lib.rs b/userland/acpiserver/aml/src/lib.rs index 76190c04632..f75947a350f 100644 --- a/userland/acpiserver/aml/src/lib.rs +++ b/userland/acpiserver/aml/src/lib.rs @@ -80,10 +80,8 @@ pub const MAX_LIVE: usize = 16 << 20; /// The bytes of work one step stands for: a step for every this many bytes /// an operation makes, copies, compares or walks. pub(crate) const WORK_PER_STEP: usize = 64; -/// The time a load, and an evaluation whose caller names no other limit, may -/// ask to Sleep, Stall and Wait, together, in µs: time asked for, whatever -/// the host then spends. -pub const MAX_WAIT_US: u64 = 10_000_000; +/// The time one evaluation may ask to Sleep, Stall and Wait, together, in µs. +pub(crate) const MAX_WAIT_US: u64 = 10_000_000; /// What the Revision opcode answers (§19.6.119): this interpreter's revision. pub(crate) const REVISION: u64 = 1; @@ -354,7 +352,7 @@ impl Interpreter { let table = self.meter.bytes(bytes)?; let (root, made) = (self.ns.root(), self.ns.mark()); let mut f = Frame::new(root, Vec::new(), table.clone(), 0); - let mut m = Machine::new(&mut self.ns, host, w, self.meter.clone(), MAX_WAIT_US); + let mut m = Machine::new(&mut self.ns, host, w, self.meter.clone()); let bytes = table.borrow(); let mut c = stream::Cursor::new(&bytes, toyos_acpi::SDT_HEADER_LEN, bytes.len()); let r = m.term_list(&mut f, &mut c).and_then(|flow| match flow { @@ -371,28 +369,20 @@ impl Interpreter { } /// Evaluates the object at an absolute path, written `\_SB.PCI0._STA`: a - /// method is invoked with `args`, anything else is its value. It may ask - /// to wait [`MAX_WAIT_US`]. + /// method is invoked with `args`, anything else is its value. pub fn evaluate(&mut self, host: &mut dyn Host, path: &str, args: &[Value]) -> Result { - self.evaluate_within(host, path, args, MAX_WAIT_US) - } - - /// [`Interpreter::evaluate`], asking to Sleep, Stall and Wait `wait_us` - /// at most, which the caller names for a method it knows to ask more - /// than [`MAX_WAIT_US`]. - pub fn evaluate_within(&mut self, host: &mut dyn Host, path: &str, args: &[Value], wait_us: u64) -> Result { self.last = Usage::default(); - let r = self.evaluate_in(host, path, args, wait_us); + let r = self.evaluate_in(host, path, args); self.last.live = self.meter.live(); r } - fn evaluate_in(&mut self, host: &mut dyn Host, path: &str, args: &[Value], wait_us: u64) -> Result { + fn evaluate_in(&mut self, host: &mut dyn Host, path: &str, args: &[Value]) -> Result { let w = self.width.ok_or(Error::Table("nothing is loaded"))?; let id = self.named(path)?; let args = args.iter().map(|a| self.object_of(a, w, 0)).collect::, _>>()?; let meter = self.meter.clone(); - let mut m = Machine::new(&mut self.ns, host, w, meter.clone(), wait_us); + let mut m = Machine::new(&mut self.ns, host, w, meter.clone()); let mut handed = 0; let r = m.evaluate(id, args).and_then(|o| value_of(&mut m, &meter, &mut handed, o, 0)); // The value is the caller's from here, and no longer this interpreter's. diff --git a/userland/acpiserver/aml/tests/hostile.rs b/userland/acpiserver/aml/tests/hostile.rs index fd98781e840..aaac51f7d98 100644 --- a/userland/acpiserver/aml/tests/hostile.rs +++ b/userland/acpiserver/aml/tests/hostile.rs @@ -532,22 +532,16 @@ fn stall(us: u64) -> Vec { cat(&[&[0x5B, 0x21], &int(us)]) } -/// The limit is the most an evaluation may ask, to the microsecond: the -/// caller's where it names one, and ten seconds where it does not, for a +/// Ten seconds is the most an evaluation may ask, to the microsecond, and a /// load too. What crosses it is not asked of the host. #[test] -fn the_wait_limit_is_exact_and_the_callers_to_name() { +fn the_wait_limit_is_exact_and_a_loads_too() { let (mut i, mut m) = loaded(&cat(&[ &method("TEN0", 0, &cat(&[&sleep(9_999), &stall(250), &stall(250), &stall(250), &stall(250), &ret(&int(1))])), &method("TEN1", 0, &cat(&[&sleep(10_000), &stall(1), &ret(&int(1))])), - &method("TWEN", 0, &cat(&[&sleep(20_000), &ret(&int(2))])), - &method("TWE1", 0, &cat(&[&sleep(20_000), &stall(1), &ret(&int(2))])), - &method("NONE", 0, &cat(&[&sleep(0), &stall(0), &ret(&int(3))])), - &method("ONE0", 0, &cat(&[&stall(1), &ret(&int(3))])), ])); let asleep = Error::Bound("more time asleep than one evaluation may spend"); let over = Err(asleep.clone()); - assert_eq!(toyos_aml::MAX_WAIT_US, 10_000_000); assert_eq!(i.evaluate(&mut m, "\\TEN0", &[]), Ok(Value::Integer(1))); assert_eq!(i.usage().waited_us, 10_000_000); @@ -555,14 +549,6 @@ fn the_wait_limit_is_exact_and_the_callers_to_name() { assert_eq!(i.evaluate(&mut m, "\\TEN1", &[]), over); assert_eq!(i.usage().waited_us, 10_000_001); assert_eq!(m.log, vec![Event::Sleep(10_000)]); - assert_eq!(i.evaluate(&mut m, "\\TWEN", &[]), over); - - assert_eq!(i.evaluate_within(&mut m, "\\TWEN", &[], 20_000_000), Ok(Value::Integer(2))); - assert_eq!(i.usage().waited_us, 20_000_000); - assert_eq!(i.evaluate_within(&mut m, "\\TWE1", &[], 20_000_000), over); - assert_eq!(i.evaluate_within(&mut m, "\\TEN0", &[], 9_999_999), over); - assert_eq!(i.evaluate_within(&mut m, "\\NONE", &[], 0), Ok(Value::Integer(3))); - assert_eq!(i.evaluate_within(&mut m, "\\ONE0", &[], 0), over); let mut m = Machine::default(); assert_eq!(Interpreter::new().load_bytes(&mut m, &dsdt(&cat(&[&sleep(10_000), &def_name("A", &int(1))]))), Ok(())); @@ -624,6 +610,11 @@ fn usage_is_each_calls_own_and_a_refusals_too() { let after = i.usage(); assert_eq!((after.waited_us, after.live), (7_000, loaded.live)); assert!(after.steps >= 3 && after.steps < 256, "{after:?}"); + + // A load refused before anything ran took nothing, whatever the call + // before it took. + assert_eq!(i.load_bytes(&mut m, &dsdt(&[])), Err(Error::Table("a second DSDT"))); + assert_eq!(i.usage(), toyos_aml::Usage { steps: 0, waited_us: 0, live: loaded.live }); } /// BLOCKER 9 (c): a reference to an object a method created names nothing diff --git a/userland/acpiserver/aml/tests/walk.rs b/userland/acpiserver/aml/tests/walk.rs index 3c46eb9ab4f..f3126359b0b 100644 --- a/userland/acpiserver/aml/tests/walk.rs +++ b/userland/acpiserver/aml/tests/walk.rs @@ -1,12 +1,13 @@ //! The namespace read from the root down (§5.3): every object once, after //! the object it is in, siblings in the order their tables declared them, //! whatever a table's names sort as, however deep it nests, and whatever a -//! refused load or a method's exit took out of the arena before. +//! refused load or a method's exit took out of the arena before; and what a +//! walk takes of the interpreter's bound, to the byte. mod common; use common::*; -use toyos_aml::{Error, Interpreter, Kind, Value}; +use toyos_aml::{Error, Interpreter, Kind, Value, MAX_LIVE}; /// What §5.3.1 and §5.7 predefine, in the order `Interpreter::new` makes them. const PREDEFINED: [(&str, Kind); 9] = [ @@ -229,6 +230,59 @@ fn a_namespace_nested_deep_is_walked_whole() { assert_eq!(depths, (1..=TABLES * LEVELS).collect::>()); } +/// Leaves the interpreter `room` bytes of its bound, by the buffers `\SET_` +/// stores in `\PKG0`: one an element while that is far off, then the last +/// element's, resized to the byte. `fill` is the next element and the last +/// one's bytes. +fn leave(i: &mut Interpreter, m: &mut Machine, fill: &mut (u64, usize), room: usize) { + const LAST: u64 = 63; + let mut set = |at: u64, bytes: usize| { + assert_eq!(i.evaluate(&mut *m, "\\SET_", &[Value::Integer(at), Value::Integer(bytes as u64)]), Ok(Value::Uninitialized)); + MAX_LIVE - i.usage().live + }; + let mut free = set(LAST, fill.1); + while free > room + 4096 { + assert!(fill.0 < LAST, "{free} bytes free with every element stored"); + free = set(fill.0, ((free - room) / 2).min(1 << 20)); + fill.0 += 1; + } + fill.1 = fill.1 + free - room; + assert_eq!(set(LAST, fill.1), room, "bytes of the bound left"); +} + +/// A walk takes two links a node and its deepest path from the bound, and +/// not a byte less: an interpreter with room for both is walked, and one +/// with a byte less, which is room for the links, refuses and holds +/// afterwards what it held before. +#[test] +fn a_walk_takes_its_links_and_its_deepest_path_from_the_bound() { + const TABLES: usize = 120; + const LEVELS: usize = 100; + let mut m = Machine::default(); + let mut i = Interpreter::new(); + let set = method("SET_", 2, &store(&buffer(&arg(1), &[]), &index(&name("PKG0"), &arg(0), &[0x00]))); + i.load_bytes(&mut m, &dsdt(&cat(&[&def_name("PKG0", &var_package(&int(64), &[])), &set, &deeper(0, LEVELS)]))).unwrap(); + for t in 1..TABLES { + i.load_bytes(&mut m, &table(b"SSDT", 2, &deeper(t * LEVELS, LEVELS))).unwrap(); + } + // The root is no entry, and has its links as every node has. + let nodes = i.walk().unwrap().count() + 1; + let (links, path) = (8 * nodes, 5 * TABLES * LEVELS); + let mut fill = (0, 8192); + + leave(&mut i, &mut m, &mut fill, links + path); + let held = i.usage().live; + assert_eq!(i.walk().map(|w| w.count()), Ok(nodes - 1)); + assert_eq!(i.evaluate(&mut m, "\\_REV", &[]), Ok(Value::Integer(2))); + assert_eq!(i.usage().live, held, "bytes held after a walk, and before it"); + + leave(&mut i, &mut m, &mut fill, links + path - 1); + let held = i.usage().live; + assert_eq!(i.walk().map(|w| w.count()), Err(Error::Bound("more held live than one interpreter holds"))); + assert_eq!(i.evaluate(&mut m, "\\_REV", &[]), Ok(Value::Integer(2))); + assert_eq!(i.usage().live, held, "bytes held after a walk refused for its path, and before it"); +} + /// A parent of thousands, declared against the order their names sort in. #[test] fn a_parent_of_thousands_is_walked_in_their_order() { From bd7b865475d4a1212193cd696e1c259c006bbf0a Mon Sep 17 00:00:00 2001 From: japabu Date: Thu, 8 Oct 2026 13:06:27 +0200 Subject: [PATCH 3/4] The track says what was read of initialisation order, and records the wait limit The walk's bullet claimed the T14's initialisation does not hang on sibling order. What was read is less: 45 `_INI`, a dry run under mostly zero answers that found two of them writing, and those two parent and child. The bullet now says that and what nothing has read. A new bullet in the interpreter stage's list records that ten seconds is the only wait limit and which slice, on what reading, brings a caller's. The lines on `main` owned by the power-off stage keep that owner: this branch lands before the slice that retires that stage. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A --- ...ine-in-acpi-mode-and-interprets-its-aml.md | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/issues/toyos-runs-the-machine-in-acpi-mode-and-interprets-its-aml.md b/issues/toyos-runs-the-machine-in-acpi-mode-and-interprets-its-aml.md index ef0002fc039..58bc61134fa 100644 --- a/issues/toyos-runs-the-machine-in-acpi-mode-and-interprets-its-aml.md +++ b/issues/toyos-runs-the-machine-in-acpi-mode-and-interprets-its-aml.md @@ -233,14 +233,27 @@ otherwise pay to find again: allocator holds the most a load and an evaluation hold while they run to it; and the arena's capacity after the real machine's last table is read, not derived. -- **The T14's tables are not in name order, and its initialisation does - not hang on the order.** The interpreter's walk of the namespace - (`Interpreter::walk`, `userland/acpiserver/aml/src/lib.rs`) reads - siblings as their tables declared them. Of the 267 objects of the T14's - tables that hold others, 217 declare them against the order their names - sort in; and the embedded controller's device is below the PCI root - bridge, the two whose `_INI` a dry run found writing anything, so a - parent is initialised before its child whichever order siblings take. +- **The T14's tables are not in name order.** The interpreter's walk of + the namespace (`Interpreter::walk`, `userland/acpiserver/aml/src/lib.rs`) + reads siblings as their tables declared them. Of the 267 objects of the + T14's tables that hold others, 217 declare them against the order their + names sort in. What its initialisation needs of the order was read only + so far: the tables define 45 `_INI`, all methods; a dry run that answered + every read zero but one word of memory and each function's header found + two of them writing anything, the PCI root bridge's and the embedded + controller's; and that controller's device is below that bridge, so + under those answers the two run parent before child whichever order + siblings take. What the 45 do under the machine's own answers, and + whether any hangs on a sibling's, nothing has read. +- **An evaluation may ask to wait ten seconds, and no caller names another + limit.** The limit counts the time asked of Sleep, Stall and Wait, a + load's too (`MAX_WAIT_US`, `userland/acpiserver/aml/src/lib.rs`), and + `Interpreter::usage` says what a call asked, the request that was refused + included. The T14's 14 tables asked for none while they loaded, and no + method of them has been read refused at it. Owner: this stage. **Exit**: + the slice that carries a reading of a method refused at ten seconds, on + the T14 and by `usage`, brings the limit its caller names with that + reading; until one does, ten seconds stands and nothing names more. What the server's load of the tables, on the T14 and through the kernel's mediated access, leaves open: From 9cf510ce1b311fbe0d7f3321db55826489e4fe08 Mon Sep 17 00:00:00 2001 From: japabu Date: Thu, 8 Oct 2026 13:33:32 +0200 Subject: [PATCH 4/4] The deep-namespace test helper asks is_multiple_of, as CI's clippy wants CI's clippy (rust 1.99) raises manual_is_multiple_of on `left % 200 == 0` in tests/walk.rs's `deeper`; the development machine's older clippy does not. `left` is a usize and the divisor a non-zero literal, so the value is the same. It is the only `%` the branch adds, and the branch adds no `wrapping_neg`. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A --- userland/acpiserver/aml/tests/walk.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/userland/acpiserver/aml/tests/walk.rs b/userland/acpiserver/aml/tests/walk.rs index f3126359b0b..3fd282899f6 100644 --- a/userland/acpiserver/aml/tests/walk.rs +++ b/userland/acpiserver/aml/tests/walk.rs @@ -198,7 +198,7 @@ fn deeper(above: usize, levels: usize) -> Vec { } let mut left = above; while left > 0 { - let chunk = if left % 200 == 0 { 200 } else { left % 200 }; + let chunk = if left.is_multiple_of(200) { 200 } else { left % 200 }; left -= chunk; let path = vec!["D"; chunk].join("."); body = scope(&if left == 0 { format!("\\{path}") } else { path }, &body);