diff --git a/src/grain/bytecode/verify.rs b/src/grain/bytecode/verify.rs index a98434fde..9a8d31264 100644 --- a/src/grain/bytecode/verify.rs +++ b/src/grain/bytecode/verify.rs @@ -1,5 +1,6 @@ use crate::grain::bytecode::code::{self, tag}; use crate::grain::bytecode::{Chain, Chunk, Op, Receiver, Root, Step, Switch, Tail}; +use crate::grain::format::Caps; #[cfg(feature = "no_std")] use std::prelude::v1::*; @@ -9,7 +10,7 @@ use std::prelude::v1::*; /// Chains and switches come through whole rather than as a count, because both /// hold things that have to be checked rather than counted: how much operand /// stack a chain consumes, and where a switch can send control. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone)] pub struct Pools<'a> { /// How many constants there are. pub consts: usize, @@ -34,6 +35,15 @@ pub struct Pools<'a> { /// anything a script can express. #[derive(Debug, Clone, PartialEq, Eq)] pub enum VerifyError { + /// An instruction requires capabilities which is unavailable. + MissingCaps { + /// Artifact declared capabilities + artifact: String, + /// Missing capabilities + missing: String, + /// Byte offset of the offending tag + at: usize, + }, /// A tag with no instruction behind it, or one whose operands run past the /// end of the chunk. Undecodable { @@ -149,7 +159,12 @@ pub enum VerifyError { /// /// Returns the measured stack high water, which is what the chunk should /// declare. -pub fn verify(code: &[u8], chunks: &[Chunk], pools: Pools) -> Result, VerifyError> { +pub fn verify( + caps: Caps, + code: &[u8], + chunks: &[Chunk], + pools: &Pools, +) -> Result, VerifyError> { // Pass one: where do instructions start? // // Over the whole buffer at once, because every chunk shares it and an @@ -171,7 +186,7 @@ pub fn verify(code: &[u8], chunks: &[Chunk], pools: Pools) -> Result, V chunks .iter() - .map(|chunk| verify_chunk(code, chunk, &starts, pools)) + .map(|chunk| verify_chunk(caps, code, chunk, &starts, pools)) .collect() } @@ -193,10 +208,11 @@ struct State { /// Walk one chunk's reachable instructions, checking that every path into an /// instruction agrees on the stack depth. fn verify_chunk( + caps: Caps, code: &[u8], chunk: &Chunk, starts: &[bool], - pools: Pools, + pools: &Pools, ) -> Result { let (entry, end) = (chunk.entry() as usize, chunk.end() as usize); if end > code.len() || entry > end { @@ -235,6 +251,16 @@ fn verify_chunk( let op = code::decode(code, at).ok_or(VerifyError::Undecodable { at })?; + let required_caps = required_caps(&op, pools); + + if !caps.contains(required_caps) { + return Err(VerifyError::MissingCaps { + at, + artifact: caps.to_string(), + missing: (required_caps - caps).to_string(), + }); + } + let (requires, pops, pushes) = effect(&op, pools); // Number of slots to pop from the stack must necessarily @@ -397,8 +423,89 @@ fn verify_chunk( Ok(high_water) } +/// Capabilities an instruction requires. +fn required_caps(op: &Op, pools: &Pools) -> Caps { + match op { + Op::Chain(index) => match pools.chains.get(*index as usize) { + Some(chain) => { + let mut caps = Caps::empty(); + + match chain.root { + Root::Local { .. } | Root::Named { .. } | Root::Temporary => {} + Root::This { .. } => caps.insert(Caps::THIS), + } + chain.steps.iter().for_each(|step| match step { + Step::Index { .. } => caps.insert(Caps::INDEXING), + Step::Property { .. } => caps.insert(Caps::PROPERTY), + Step::Method { .. } => caps.insert(Caps::METHOD), + }); + + caps + } + None => Caps::empty(), + }, + + Op::Const(..) + | Op::Unit + | Op::Bool(..) + | Op::LoadLocal(..) + | Op::LoadNamed(..) + | Op::StoreLocal { .. } + | Op::DeclareLocal { .. } + | Op::Pop + | Op::AssignLocal { .. } + | Op::AssignNamed { .. } + | Op::AssignThis { .. } + | Op::JumpIfFalse { .. } + | Op::JumpIfTrue { .. } + | Op::Switch(..) + | Op::Jump(..) + | Op::UnwindTo(..) + | Op::Tick + | Op::Checkpoint + | Op::PushHandler { .. } + | Op::PopHandler + | Op::SkipIfNotUnit { .. } + | Op::Call { .. } + | Op::CallFnPtr { .. } + | Op::Rotate(..) + | Op::CheckSize { .. } + | Op::InterpolateStart + | Op::InterpolateAppend + | Op::InterpolateEnd + | Op::MakeFnPtr + | Op::MakeClosure(..) + | Op::Curry(..) + | Op::Throw + | Op::IterInit + | Op::IterNext { .. } + | Op::IterDrop + | Op::Return + | Op::LoadShared(..) + | Op::LoadSharedNamed(..) + | Op::StoreShared(..) + | Op::Statement { .. } => Caps::empty(), + + // `EvalAst` is a host-only instruction, so it is never in an artifact. + Op::EvalAst { .. } => Caps::empty(), + + Op::Share(..) | Op::ShareNamed(..) => Caps::SHARING, + + Op::RequireThis | Op::LoadThis | Op::LoadThisShared => Caps::THIS, + + Op::CallRef { receiver, .. } => match receiver { + Receiver::Local(..) | Receiver::Named(..) => Caps::empty(), + Receiver::This => Caps::THIS, + }, + + Op::MakeArray(..) => Caps::ARRAY, + Op::MakeMap(..) => Caps::MAP, + Op::IsShared => Caps::SHARING, + } +} + /// How many operands an instruction requires, consumes and produces. -fn effect(op: &Op, pools: Pools) -> (usize, usize, usize) { +fn effect(op: &Op, pools: &Pools) -> (usize, usize, usize) { match op { // A chain eats the indices and arguments its steps named, plus a root // that is not a slot, plus the value being assigned, and leaves one @@ -515,7 +622,7 @@ fn effect(op: &Op, pools: Pools) -> (usize, usize, usize) { /// The VM treats these as assertions, and an artifact is the one place they can /// be wrong without a compiler bug. Reads the operands off the bytes rather /// than off a decoded `Op`, so it runs in the same pass that measures widths. -fn check_indices(at: usize, code: &[u8], pools: Pools) -> Result<(), VerifyError> { +fn check_indices(at: usize, code: &[u8], pools: &Pools) -> Result<(), VerifyError> { let index = |offset: usize| code::u16_at(code, at + offset).map_or(0, u32::from); let bounded = |index: u32, what: &'static str, len: usize| { if index as usize >= len { @@ -582,7 +689,7 @@ fn check_indices(at: usize, code: &[u8], pools: Pools) -> Result<(), VerifyError /// A chain is one instruction over an unbounded record, so nearly all of what /// it names lives in the pool rather than in the code. Bounding only the /// record's own index would leave most of the instruction unverified. -fn check_chain_indices(at: usize, chain: &Chain, pools: Pools) -> Result<(), VerifyError> { +fn check_chain_indices(at: usize, chain: &Chain, pools: &Pools) -> Result<(), VerifyError> { let bounded = |index: u32, what: &'static str, len: usize| { if index as usize >= len { Err(VerifyError::BadIndex { at, what, index }) @@ -628,6 +735,7 @@ fn check_chain_indices(at: usize, chain: &Chain, pools: Pools) -> Result<(), Ver mod tests { use super::*; use crate::grain::bytecode::assemble; + use crate::grain::format::Abi; fn pools() -> Pools<'static> { Pools { @@ -645,14 +753,14 @@ mod tests { fn check(ops: Vec) -> Result, VerifyError> { let (code, _) = assemble(&ops).expect("the test ops must assemble"); let chunk = Chunk::new(0, code.len() as u32, 8); - verify(&code, &[chunk], pools()) + verify(Abi::host().caps, &code, &[chunk], &pools()) } /// The same, for bytes `assemble` would refuse to produce — which is what /// a corrupt artifact hands the loader. fn check_bytes(code: Vec, max_stack: u16) -> Result, VerifyError> { let chunk = Chunk::new(0, code.len() as u32, max_stack); - verify(&code, &[chunk], pools()) + verify(Abi::host().caps, &code, &[chunk], &pools()) } #[test] @@ -663,6 +771,7 @@ mod tests { /// `this` is a register, so reading it costs a push and nothing else, and /// assigning to it consumes one without leaving anything behind. #[test] + #[cfg(not(feature = "no_function"))] fn the_this_register_is_reached_without_touching_the_scope() { assert_eq!(check(vec![Op::LoadThis, Op::Return]), Ok(vec![1])); assert_eq!(check(vec![Op::LoadThisShared, Op::Return]), Ok(vec![1])); @@ -761,7 +870,7 @@ mod tests { }; assert!( matches!( - verify(&code, &[chunk], pools), + verify(Abi::host().caps, &code, &[chunk], &pools), Err(VerifyError::BadIndex { what: "name", index: 3, @@ -782,9 +891,10 @@ mod tests { let chunk = Chunk::new(0, code.len() as u32, 8); assert!(matches!( verify( + Abi::host().caps, &code, &[chunk], - Pools { + &Pools { names: 1, chains: core::slice::from_ref(&assigning), ..pools() @@ -814,7 +924,7 @@ mod tests { Chunk::new(boundary, code.len() as u32, 8), ]; assert!(matches!( - verify(&code, &chunks, pools()), + verify(Abi::host().caps, &code, &chunks, &pools()), Err(VerifyError::JumpOutOfRange { .. }), )); } @@ -913,9 +1023,10 @@ mod tests { let good = [table(offsets[2], offsets[4])]; assert_eq!( verify( + Abi::host().caps, &code, &[chunk], - Pools { + &Pools { switches: &good, ..pools() } @@ -928,9 +1039,10 @@ mod tests { assert!( matches!( verify( + Abi::host().caps, &code, &[chunk], - Pools { + &Pools { switches: &mid, ..pools() } @@ -945,9 +1057,10 @@ mod tests { assert!( matches!( verify( + Abi::host().caps, &code, &[chunk], - Pools { + &Pools { switches: &outside, ..pools() } @@ -1003,9 +1116,10 @@ mod tests { let chunk = Chunk::new(0, code.len() as u32, 8); assert!(matches!( verify( + Abi::host().caps, &code, &[chunk], - Pools { + &Pools { consts: 1, ..pools() } @@ -1050,7 +1164,7 @@ mod tests { fn rejects_a_chunk_that_names_code_it_does_not_have() { let (code, _) = assemble(&[Op::Unit, Op::Return]).unwrap(); assert!(matches!( - verify(&code, &[Chunk::new(0, 9999, 8)], pools()), + verify(Abi::host().caps, &code, &[Chunk::new(0, 9999, 8)], &pools()), Err(VerifyError::ChunkOutOfRange { .. }), )); } diff --git a/src/grain/compile/mod.rs b/src/grain/compile/mod.rs index 48c945c9d..48ccbaeeb 100644 --- a/src/grain/compile/mod.rs +++ b/src/grain/compile/mod.rs @@ -22,6 +22,7 @@ use crate::grain::bytecode::{ }; use crate::grain::compile::poolable::is_poolable; use crate::grain::compile::slots::Slots; +use crate::grain::format::Caps; use crate::grain::program::{Function, Parts, Program}; /// Whether a variable reference is module-qualified, as in `foo::bar`. @@ -94,18 +95,20 @@ impl Compiler { .collect(); #[cfg(feature = "no_function")] let script_fns: Vec = Vec::new(); - let fresh = || Lowering { + + let fresh = |caps| Lowering { script_fns: script_fns.clone(), + caps, ..Lowering::default() }; - let mut lowering = fresh(); + let mut lowering = fresh(Caps::empty()); // Anything the slot model cannot account for costs the whole program // its lowering rather than risking a scope it resolved slots against // being a different shape at runtime. Coverage is preserved either way. if !lowering.program(ast.statements(), true) { - lowering = fresh(); + lowering = fresh(lowering.caps); lowering.whole_program_residual(ast.statements()); } let main_ops = lowering.code.len(); @@ -135,7 +138,7 @@ impl Compiler { let (code, offsets, main_ops, functions, skipped) = match assembled { Some((code, offsets)) => (code, offsets, main_ops, functions, skipped), None => { - lowering = fresh(); + lowering = fresh(lowering.caps); lowering.whole_program_residual(ast.statements()); let (code, offsets) = assemble(&lowering.code).expect("the fallback is one instruction"); @@ -198,6 +201,7 @@ impl Compiler { }; let mut program = Program::new( + lowering.caps, code.into(), main, functions, @@ -281,6 +285,10 @@ struct LoweredFn { #[derive(Default)] struct Lowering { + /// Capabilities required by the instructions emitted so far. + /// The compiler does not know what the caller will do with the output, + /// so it has to assume the worst and report everything it uses. + caps: Caps, code: Vec, /// One per instruction, parallel to `code`. Most are `NONE`; the dense /// shape is what makes a lookup an index, and it compacts on the way out. @@ -370,7 +378,7 @@ impl Lowering { /// nested node's `lhs` is the *current* step's operand and its `rhs` is the /// continuation. [`flatten_chain`] unpicks that into steps. fn chain(&mut self, expr: &Expr, tail: Tail, value: Option<&Expr>) -> bool { - let Some((root, steps)) = flatten_chain(expr) else { + let Some((root, steps)) = flatten_chain(self, expr) else { return false; }; @@ -402,7 +410,10 @@ impl Lowering { }, None => return false, }, - Expr::ThisPtr(pos) => Root::This { pos: *pos }, + Expr::ThisPtr(pos) => { + self.caps.insert(Caps::THIS); + Root::This { pos: *pos } + } // A qualified root resolves against imported modules, which need // `import` — the escape hatch's job. Expr::Variable(..) => return false, @@ -458,6 +469,7 @@ impl Lowering { for step in &steps { match step { ChainStep::Index(index, bracket, flags) => { + self.caps.insert(Caps::INDEXING); self.expression(index); lowered.push(Step::Index { operand: operands, @@ -468,6 +480,7 @@ impl Lowering { operands += 1; } ChainStep::Property(prop, pos, flags) => { + self.caps.insert(Caps::PROPERTY); let (getter, setter, name) = &**prop; lowered.push(Step::Property { name: self.push_name(name.clone()), @@ -478,6 +491,7 @@ impl Lowering { }); } ChainStep::Method(call, pos, flags) => { + self.caps.insert(Caps::METHOD); if !self.is_lowerable_call(call) { if value_slot.is_some() { self.rewind(rewind_mark); @@ -1004,6 +1018,8 @@ impl Lowering { // parser puts it there too (`parser.rs:2002`), and because the // chain arm below would otherwise take `this.x = 1`'s sibling. Stmt::Assignment(payload) if matches!(&payload.1.lhs, Expr::ThisPtr(..)) => { + self.caps.insert(Caps::THIS); + let (op_info, binary) = &**payload; // Before the right-hand side, not after. Rhai checks that @@ -1075,6 +1091,12 @@ impl Lowering { Stmt::Assignment(payload) if matches!(&payload.1.lhs, Expr::Dot(..) | Expr::Index(..)) => { + if matches!(&payload.1.lhs, Expr::Dot(..)) { + self.caps.insert(Caps::PROPERTY); + } else { + self.caps.insert(Caps::INDEXING); + } + let (op_info, binary) = &**payload; let op = self.op_assignment(op_info); @@ -1096,6 +1118,8 @@ impl Lowering { // closure's captures (`parser.rs:3707`). #[cfg(not(feature = "no_closure"))] Stmt::Share(names) => { + self.caps.insert(Caps::SHARING); + for (ident, ..) in names.iter() { match self.slots.resolve(&ident.name) { Some(slot) => self.emit_at(Op::Share(slot), ident.pos), @@ -1425,7 +1449,10 @@ impl Lowering { // fragment. Refusing the lowering hands the body to the walker // whole, which is where the alias lives long enough to be used. #[cfg(not(feature = "no_module"))] - Stmt::Import(..) => false, + Stmt::Import(..) => { + self.caps.insert(Caps::IMPORT); + false + } // Not lowered yet, and listed rather than matched with `_` on // purpose. A wildcard here silently turned `import` and `eval` @@ -1452,6 +1479,7 @@ impl Lowering { #[cfg(not(feature = "no_module"))] other @ Stmt::Export(..) => { + self.caps.insert(Caps::EXPORT); let residual = self.push_residual(wrap_statements(vec![other.clone()])); self.emit(Op::EvalAst { residual, @@ -1474,7 +1502,10 @@ impl Lowering { // Rhai has no float literal to parse under `no_float`, so there is // no variant to match. #[cfg(not(feature = "no_float"))] - Expr::FloatConstant(value, ..) => self.constant(Dynamic::from(**value)), + Expr::FloatConstant(value, ..) => { + self.caps.insert(Caps::FLOAT); + self.constant(Dynamic::from(**value)) + } // Folded by the optimizer, so it can hold anything a constant call // returned — including a function pointer, which must not be // copied out of a pool. See `poolable`. @@ -1513,6 +1544,22 @@ impl Lowering { } Expr::DynamicConstant(value, ..) if is_poolable(value) => { + #[cfg(not(feature = "no_index"))] + if value.is_array() { + self.caps.insert(Caps::ARRAY); + } + #[cfg(not(feature = "no_index"))] + if value.is_blob() { + self.caps.insert(Caps::BLOB); + } + #[cfg(not(feature = "no_object"))] + if value.is_map() { + self.caps.insert(Caps::MAP); + } + #[cfg(feature = "decimal")] + if value.is_decimal() { + self.caps.insert(Caps::DECIMAL); + } self.constant((**value).clone()); } @@ -1562,7 +1609,10 @@ impl Lowering { // A literal whose elements are all constant never reaches here — // Rhai's optimizer folds it into a `DynamicConstant` first — so // this is the one that has to be built at run time. + #[cfg(not(feature = "no_index"))] Expr::Array(elements, ..) if elements.len() <= u16::MAX as usize => { + self.caps.insert(Caps::ARRAY); + for (index, element) in elements.iter().enumerate() { self.expression(element); // Positioned at the element, because that is what Rhai @@ -1587,6 +1637,8 @@ impl Lowering { // one with a single computed value does, and used to fragment. #[cfg(not(feature = "no_object"))] Expr::Map(entries, ..) if entries.0.len() <= u16::MAX as usize => { + self.caps.insert(Caps::MAP); + let (computed, template) = &**entries; let template = Dynamic::from_map(template.clone()); // A template whose constants the pool cannot hold is a program @@ -1644,6 +1696,8 @@ impl Lowering { if matches!(m.name.as_str(), "call" | "curry") && m.args.len() <= u8::MAX as usize) => { + self.caps.insert(Caps::METHOD); + let Expr::MethodCall(method, ..) = &binary.rhs else { unreachable!("checked by the guard"); }; @@ -1693,6 +1747,12 @@ impl Lowering { } Expr::Dot(..) | Expr::Index(..) => { + if matches!(expr, Expr::Dot(..)) { + self.caps.insert(Caps::PROPERTY); + } else { + self.caps.insert(Caps::INDEXING); + } + // A chain emits its own operands, so a failed attempt has to // leave nothing behind. let mark = self.mark(); @@ -1725,7 +1785,10 @@ impl Lowering { // The frame's receiver, flattened as every consumer but three // wants it — see [`Op::LoadThis`] and `unflattened` below. Its own // position, because that is what `ErrorUnboundThis` carries. - Expr::ThisPtr(pos) => self.emit_at(Op::LoadThis, *pos), + Expr::ThisPtr(pos) => { + self.caps.insert(Caps::THIS); + self.emit_at(Op::LoadThis, *pos) + } Expr::MethodCall(..) | Expr::Property(..) @@ -1743,7 +1806,10 @@ impl Lowering { /// mutates a copy in the walker too, so there is nothing to carry back. fn fn_ptr_receiver(&mut self, receiver: &Expr) -> Option { match receiver { - Expr::ThisPtr(..) => Some(Receiver::This), + Expr::ThisPtr(..) => { + self.caps.insert(Caps::THIS); + Some(Receiver::This) + } Expr::Variable(payload, ..) if !has_namespace!(payload) => { match self.slots.resolve(&payload.1) { Some(slot) => Some(Receiver::Local(slot)), @@ -1775,6 +1841,7 @@ impl Lowering { // reach this instruction because `call`/`curry` go through // `Op::CallFnPtr` and `is_lowerable_call` refuses them here. if let Some(Expr::ThisPtr(..)) = call.args.first() { + self.caps.insert(Caps::THIS); return Some(Receiver::This); } @@ -1913,7 +1980,10 @@ impl Lowering { // The receiver can be a shared cell too — a closure capturing the // variable a method was called on — and the three readers that come // through here have to see the cell rather than what it holds. - Expr::ThisPtr(pos) => self.emit_at(Op::LoadThisShared, *pos), + Expr::ThisPtr(pos) => { + self.caps.insert(Caps::THIS); + self.emit_at(Op::LoadThisShared, *pos) + } other => self.expression(other), } } @@ -1940,6 +2010,7 @@ impl Lowering { // fails it. Lowering it would answer a question Rhai refuses. #[cfg(not(feature = "no_closure"))] (crate::engine::KEYWORD_IS_SHARED, 1) => { + self.caps.insert(Caps::SHARING); self.unflattened(&call.args[0]); self.emit_at(Op::IsShared, pos); } @@ -2342,18 +2413,30 @@ enum ChainStep<'a> { /// steps (`eval/chaining.rs:698`). /// /// Returns `None` for a dot onto anything but a property or a method. -fn flatten_chain(expr: &Expr) -> Option<(&Expr, Vec>)> { +fn flatten_chain<'a>( + lowering: &mut Lowering, + expr: &'a Expr, +) -> Option<(&'a Expr, Vec>)> { /// A chain node's parts: operand side, continuation side, and whether the /// step it introduces is a property rather than an index. - fn parts(expr: &Expr) -> Option<(&Expr, &Expr, ASTFlags, bool)> { + fn parts<'a>( + lowering: &mut Lowering, + expr: &'a Expr, + ) -> Option<(&'a Expr, &'a Expr, ASTFlags, bool)> { match expr { - Expr::Dot(binary, flags, ..) => Some((&binary.lhs, &binary.rhs, *flags, true)), - Expr::Index(binary, flags, ..) => Some((&binary.lhs, &binary.rhs, *flags, false)), + Expr::Dot(binary, flags, ..) => { + lowering.caps.insert(Caps::METHOD); + Some((&binary.lhs, &binary.rhs, *flags, true)) + } + Expr::Index(binary, flags, ..) => { + lowering.caps.insert(Caps::INDEXING); + Some((&binary.lhs, &binary.rhs, *flags, false)) + } _ => None, } } - let (root, mut rest, mut flags, mut dotted) = parts(expr)?; + let (root, mut rest, mut flags, mut dotted) = parts(lowering, expr)?; let mut steps = Vec::new(); // Rhai's `op_pos`, which is the position of the chain node the step is // being taken *inside* rather than of the step's operand, and which walks @@ -2371,7 +2454,7 @@ fn flatten_chain(expr: &Expr) -> Option<(&Expr, Vec>)> { // node is not marked as the last one. Otherwise it is this step's own // operand — the index expression, or the property being read. let next = (!flags.contains(ASTFlags::BREAK)) - .then(|| parts(rest)) + .then(|| parts(lowering, rest)) .flatten(); let (operand, following) = match next { @@ -2380,18 +2463,27 @@ fn flatten_chain(expr: &Expr) -> Option<(&Expr, Vec>)> { }; steps.push(match (dotted, operand) { - (true, Expr::Property(prop, pos)) => ChainStep::Property(prop, *pos, step_flags), - (true, Expr::MethodCall(call, pos)) => ChainStep::Method(call, *pos, step_flags), + (true, Expr::Property(prop, pos)) => { + lowering.caps.insert(Caps::PROPERTY); + ChainStep::Property(prop, *pos, step_flags) + } + (true, Expr::MethodCall(call, pos)) => { + lowering.caps.insert(Caps::METHOD); + ChainStep::Method(call, *pos, step_flags) + } // `a.(expr)` is not syntax, so a dot onto anything else is a shape // the parser only makes for something handled elsewhere. (true, _) => return None, - (false, index) => ChainStep::Index(index, bracket, step_flags), + (false, index) => { + lowering.caps.insert(Caps::INDEXING); + ChainStep::Index(index, bracket, step_flags) + } }); match following { Some(node) => { let (_, next_rest, next_flags, next_dotted) = - parts(node).expect("checked by `next`"); + parts(lowering, node).expect("checked by `next`"); rest = next_rest; flags = next_flags; dotted = next_dotted; diff --git a/src/grain/format/abi.rs b/src/grain/format/abi.rs index 77158cfd7..4d1d773e8 100644 --- a/src/grain/format/abi.rs +++ b/src/grain/format/abi.rs @@ -1,49 +1,129 @@ -//! What a `Dynamic` is, on the machine that wrote the artifact. -//! -//! Rhai's feature flags change the value representation rather than just what -//! is available: `f32_float` makes `FLOAT` an `f32`, `only_i32` narrows `INT`, -//! `sync` swaps `Rc` for `Arc`. Loading an artifact across one of those is not -//! a missing feature, it is a value decoded as the wrong type — so the header -//! carries a fingerprint and the loader refuses a mismatch by name. -//! -//! ## What the fingerprint can and cannot see -//! -//! Widths are *measured*, so they are right no matter how Rhai was configured. -//! The booleans are read from this crate's own features, which is why the -//! manifest mirrors them — `cfg!(feature = "no_object")` here does not consult -//! Rhai's manifest. -//! -//! That leaves one gap: enabling a restriction on Rhai directly, bypassing the -//! mirror. The cross-checks below close it wherever rust can prove the -//! disagreement, turning it into a compile error rather than a wrong -//! fingerprint. They cannot close it everywhere, which is what the mirror is -//! documented for. - -/// Restrictions that are not visible in a width. -/// -/// Order is the wire order and must never change; append only. A flag's name -/// is what the loader reports, so it has to match Rhai's own spelling. -const FLAGS: &[(&str, bool)] = &[ - ("sync", cfg!(feature = "sync")), - ("decimal", cfg!(feature = "decimal")), - ("no_index", cfg!(feature = "no_index")), - ("no_object", cfg!(feature = "no_object")), - ("no_closure", cfg!(feature = "no_closure")), - ("no_function", cfg!(feature = "no_function")), - ("no_module", cfg!(feature = "no_module")), - ("no_position", cfg!(feature = "no_position")), - ("no_custom_syntax", cfg!(feature = "no_custom_syntax")), - ("no_time", cfg!(feature = "no_time")), - ("unchecked", cfg!(feature = "unchecked")), -]; +//! Capabilities that a script requires to run. + +use bitflags::bitflags; +#[cfg(feature = "no_std")] +use std::prelude::v1::*; + +bitflags! { + /// Capability flags. + /// + /// Order is the wire order and must never change; append only. + #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct Caps: u32 { + /// The script uses floating-point numbers, which are not available under `no_float`. + const FLOAT = 1<<0; + /// The script uses arrays, which are not available under `no_index`. + const ARRAY = 1<<1; + /// The script uses BLOB's, which are not available under `no_index`. + const BLOB = 1<<2; + /// The script uses object maps, which are not available under `no_object`. + const MAP = 1<<3; + /// The script uses decimal numbers, which are only available under `decimal`. + const DECIMAL = 1<<4; + /// The script defines functions, which are not available under `no_function`. + const DEFINE_FUNCTION = 1<<5; + /// The script employs indexing, which is not available under `no_index`. + const INDEXING = 1<<6; + /// The script accesses properties, which are not available under `no_object`. + const PROPERTY = 1<<7; + /// The script uses method calling style, which is not available under `no_object`. + const METHOD = 1<<8; + /// The script uses `this`, which is not available under `no_function`. + const THIS = 1<<9; + /// The script uses shared values, which is not available under `no_closure`. + const SHARING = 1<<10; + /// The script uses the `import` statement to import modules, which is not available under `no_module`. + const IMPORT = 1<<11; + /// The script uses the `export` statement to export in modules, which is not available under `no_module`. + const EXPORT = 1<<12; + /// The script uses the custom syntax, which is not available under `no_custom_syntax`. + const CUSTOM_SYNTAX = 1<<13; + } +} -/// `Engine` is only `Send + Sync` when Rhai is built with `sync`, so claiming -/// the flag without Rhai agreeing fails to compile. -#[cfg(feature = "sync")] -const _: () = { - const fn assert_sync() {} - let _ = assert_sync::; -}; +impl std::fmt::Display for Caps { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + let description = CAP_FLAGS + .iter() + .filter(|(cap, _, _)| self.contains(*cap)) + .map(|(_, name, _)| *name) + .collect::>() + .join(", "); + + f.write_str(&description)?; + + // A bit not in the table means the writer knows a capability this build does not. + // Reporting it as unknown. + if !(*self - Self::all()).is_empty() { + if !description.is_empty() { + f.write_str(", ")?; + } + f.write_str("requires an unknown capability")?; + } + + if description.is_empty() { + f.write_str("requires nothing")?; + } + + Ok(()) + } +} + +/// A table of all capabilities, their human-readable names, +/// and whether this build has them. +const CAP_FLAGS: &[(Caps, &'static str, bool)] = &[ + ( + Caps::FLOAT, + "uses floating-point numbers", + !cfg!(feature = "no_float"), + ), + (Caps::ARRAY, "uses arrays", !cfg!(feature = "no_index")), + (Caps::BLOB, "uses BLOB's", !cfg!(feature = "no_index")), + (Caps::MAP, "uses object maps", !cfg!(feature = "no_object")), + ( + Caps::DECIMAL, + "uses decimal numbers", + cfg!(feature = "decimal"), + ), + ( + Caps::DEFINE_FUNCTION, + "defines functions", + !cfg!(feature = "no_function"), + ), + (Caps::INDEXING, "uses indexing", !cfg!(feature = "no_index")), + ( + Caps::PROPERTY, + "accesses properties", + !cfg!(feature = "no_object"), + ), + ( + Caps::METHOD, + "uses method calling style", + !cfg!(feature = "no_object"), + ), + (Caps::THIS, "uses `this`", !cfg!(feature = "no_function")), + ( + Caps::SHARING, + "uses shared values", + !cfg!(feature = "no_closure"), + ), + ( + Caps::IMPORT, + "imports modules", + !cfg!(feature = "no_module"), + ), + ( + Caps::EXPORT, + "exports data in modules", + !cfg!(feature = "no_module"), + ), + ( + Caps::CUSTOM_SYNTAX, + "uses custom syntax", + // Unsupported syntax is not yet supported + false && !cfg!(feature = "no_custom_syntax"), + ), +]; /// The value representation an artifact was written against. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -53,8 +133,8 @@ pub struct Abi { pub int_bytes: u8, /// `size_of::()`, or 0 under `no_float`. pub float_bytes: u8, - /// `FLAGS` as a bitmask, low bit first. - pub flags: u32, + /// current build's feature flags. + pub caps: Caps, } /// How two fingerprints differ. @@ -64,7 +144,7 @@ pub struct Abi { #[derive(Debug, Clone, PartialEq, Eq)] pub enum AbiMismatch { /// A width differs, which means integers or floats would decode wrong. - Width { + DataWidth { /// Which width differs what: &'static str, /// What the writer used @@ -72,35 +152,30 @@ pub enum AbiMismatch { /// What this build uses host: u8, }, - /// A restriction differs. `artifact` is whether the writer had it on. - Flag { - /// Which flag differs - flag: &'static str, - /// Whether the writer had it on - artifact: bool, + /// Missing capabilities, which means the artifact uses features this build does not have. + MissingCaps { + /// Which capabilities are missing. + caps: String, }, } impl core::fmt::Display for AbiMismatch { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { - Self::Width { + Self::DataWidth { what, artifact, host, } => write!( f, - "artifact was written with a {artifact}-byte {what}, but this build has {host}" + "artifact cannot load because it was written with a {artifact}-byte {what}, but this build has {host}" ), - Self::Flag { flag, artifact } => { - let (writer, reader) = if *artifact { - ("on", "off") - } else { - ("off", "on") - }; + Self::MissingCaps { + caps: capability, + } => { write!( f, - "artifact was written with `{flag}` {writer}, but this build has it {reader}" + "artifact cannot load because {capability}, but this build does not have the necessary features enabled" ) } } @@ -111,22 +186,19 @@ impl Abi { /// The fingerprint of the running build. #[must_use] pub fn host() -> Self { - #[cfg(not(feature = "no_float"))] - let float_bytes = core::mem::size_of::() as u8; - #[cfg(feature = "no_float")] - let float_bytes = 0u8; + let mut caps = Caps::empty(); - let mut flags = 0u32; - for (bit, (_, on)) in FLAGS.iter().enumerate() { - if *on { - flags |= 1 << bit; - } - } + CAP_FLAGS + .iter() + .for_each(|(flag, _, on)| caps.set(*flag, *on)); Self { int_bytes: core::mem::size_of::() as u8, - float_bytes, - flags, + #[cfg(not(feature = "no_float"))] + float_bytes: core::mem::size_of::() as u8, + #[cfg(feature = "no_float")] + float_bytes: 0, + caps, } } @@ -135,33 +207,26 @@ impl Abi { /// Widths first: they are measured rather than declared, so they are the /// claim least likely to be lying. #[must_use] - pub fn incompatible_with(self, host: Self) -> Option { + pub fn is_incompatible_with(self, host: Self) -> Option { if self.int_bytes != host.int_bytes { - return Some(AbiMismatch::Width { - what: "INT", + return Some(AbiMismatch::DataWidth { + what: "integer", artifact: self.int_bytes, host: host.int_bytes, }); } if self.float_bytes != host.float_bytes { - return Some(AbiMismatch::Width { - what: "FLOAT", + return Some(AbiMismatch::DataWidth { + what: "floating-point number", artifact: self.float_bytes, host: host.float_bytes, }); } - let differing = self.flags ^ host.flags; - if differing != 0 { - let bit = differing.trailing_zeros() as usize; - // A bit past the table means the writer knew a flag this build does - // not. Reporting it as unknown beats indexing out of bounds. - let flag = FLAGS - .get(bit) - .map_or("an unknown restriction", |(name, _)| *name); - return Some(AbiMismatch::Flag { - flag, - artifact: self.flags & (1 << bit) != 0, + let missing = self.caps - host.caps; + if !missing.is_empty() { + return Some(AbiMismatch::MissingCaps { + caps: missing.to_string(), }); } @@ -175,7 +240,7 @@ mod tests { #[test] fn a_build_can_load_its_own_artifacts() { - assert_eq!(Abi::host().incompatible_with(Abi::host()), None); + assert_eq!(Abi::host().is_incompatible_with(Abi::host()), None); } #[test] @@ -204,9 +269,9 @@ mod tests { ..host }; assert_eq!( - narrow.incompatible_with(host), - Some(AbiMismatch::Width { - what: "INT", + narrow.is_incompatible_with(host), + Some(AbiMismatch::DataWidth { + what: "integer", artifact: host.int_bytes / 2, host: host.int_bytes, }), @@ -216,31 +281,42 @@ mod tests { /// The message has to name the flag; that is the difference between an /// error a user can act on and one they cannot. #[test] + #[cfg(not(feature = "no_object"))] fn a_differing_restriction_is_refused_by_name() { - let host = Abi::host(); - let restricted = Abi { - flags: host.flags ^ (1 << 3), - ..host + let cap = Caps::MAP; + let desc = CAP_FLAGS + .iter() + .find(|(flag, _, _)| *flag == cap) + .expect("the test is broken") + .1; + + // Let's say we need all caps on the host. + let needed = Abi::host(); + + // But the host has one less than we need, so it cannot load the artifact. + let host = Abi { + caps: needed.caps - cap, + ..needed }; - let Some(mismatch @ AbiMismatch::Flag { flag, .. }) = restricted.incompatible_with(host) + let Some(AbiMismatch::MissingCaps { caps: missing, .. }) = + needed.is_incompatible_with(host) else { - panic!("a differing flag must be refused"); + panic!("a missing cap must be refused"); }; - assert_eq!(flag, "no_object"); - assert!(mismatch.to_string().contains("no_object")); + assert_eq!(missing, desc); } #[test] fn a_flag_this_build_has_never_heard_of_does_not_panic() { let host = Abi::host(); let future = Abi { - flags: host.flags ^ (1 << 31), + caps: host.caps ^ Caps::from_bits_retain(1_u32 << 31), ..host }; assert!(matches!( - future.incompatible_with(host), - Some(AbiMismatch::Flag { .. }), + future.is_incompatible_with(host), + Some(AbiMismatch::MissingCaps { .. }), )); } } diff --git a/src/grain/format/mod.rs b/src/grain/format/mod.rs index 7df142e0e..09e3d7bc3 100644 --- a/src/grain/format/mod.rs +++ b/src/grain/format/mod.rs @@ -47,7 +47,7 @@ mod abi; mod read; mod write; -pub use abi::{Abi, AbiMismatch}; +pub use abi::{Abi, AbiMismatch, Caps}; pub use read::ReadError; pub use write::WriteError; @@ -63,7 +63,7 @@ const MAGIC: [u8; 4] = *b"RGRN"; /// Bumped when an encoding changes in a way an older reader would misread. /// Additive changes that an older reader would reject anyway — a new op tag, /// a new constant tag — do not need it. -const VERSION: u16 = 9; +const VERSION: u16 = 10; /// Where a chain starts. Append only. mod root_tag { diff --git a/src/grain/format/read.rs b/src/grain/format/read.rs index 02e51d87d..2730a2e7d 100644 --- a/src/grain/format/read.rs +++ b/src/grain/format/read.rs @@ -7,7 +7,7 @@ use crate::grain::bytecode::{ AssignOp, BadTable, Chain, Chunk, Positions, Root, Step, StepFlags, Strings, Switch, SwitchCase, SwitchRange, TableError, Tail, VerifyError, }; -use crate::grain::format::abi::{Abi, AbiMismatch}; +use crate::grain::format::abi::{Abi, AbiMismatch, Caps}; use crate::grain::format::{constant, root_tag, step_tag, tail_tag, Cursor, MAGIC, VERSION}; use crate::grain::program::{Function, Parts, Program}; @@ -135,12 +135,14 @@ pub(super) fn read(bytes: &[u8]) -> Result, ReadError> { // Before anything is decoded: past here every value is read as a type the // fingerprint just promised. - let abi = Abi { + let artifact_abi = Abi { int_bytes: cursor.byte()?, float_bytes: cursor.byte()?, - flags: u32::from_le_bytes(cursor.take(4)?.try_into().expect("four bytes")), + caps: Caps::from_bits_retain(u32::from_le_bytes( + cursor.take(4)?.try_into().expect("four bytes"), + )), }; - if let Some(mismatch) = abi.incompatible_with(Abi::host()) { + if let Some(mismatch) = artifact_abi.is_incompatible_with(Abi::host()) { return Err(ReadError::Abi(mismatch)); } @@ -235,6 +237,7 @@ pub(super) fn read(bytes: &[u8]) -> Result, ReadError> { } let program = Program::new( + artifact_abi.caps, code.into(), main, functions, diff --git a/src/grain/format/write.rs b/src/grain/format/write.rs index c80efda99..6bfe20709 100644 --- a/src/grain/format/write.rs +++ b/src/grain/format/write.rs @@ -111,7 +111,7 @@ pub(super) fn write(program: &Program, positions: Positions) -> Result, let abi = Abi::host(); out.push(abi.int_bytes); out.push(abi.float_bytes); - out.extend_from_slice(&abi.flags.to_le_bytes()); + out.extend_from_slice(&program.caps().bits().to_le_bytes()); // All artifacts must know their debug ID in case they are stripped out.extend_from_slice(&program.debug_id().to_le_bytes()); diff --git a/src/grain/program.rs b/src/grain/program.rs index 7abf46ec7..e6565601b 100644 --- a/src/grain/program.rs +++ b/src/grain/program.rs @@ -10,7 +10,7 @@ use crate::grain::bytecode::{ site_to_position, sites, AssignOp, Chain, Chunk, Code, Op, Pools, Positions, Root, Strings, Switch, TableError, }; -use crate::grain::format::Sidecar; +use crate::grain::format::{Caps, Sidecar}; /// Rhai's own `SharedModule`, which it does not re-export. pub(crate) type SharedModule = Shared; @@ -73,6 +73,9 @@ pub struct Function { /// artifact format refuses to write a `Program` that has any, so nothing /// reaching a device can depend on them. pub struct Program<'a> { + /// The capabilities required by this program's instructions. + caps: Caps, + /// Every chunk's instructions, concatenated: main first, then each /// function. One buffer means one position table and one instruction /// address, so a device that fails reports a single number. @@ -306,6 +309,7 @@ pub(crate) struct Parts<'a> { impl<'a> Program<'a> { pub(crate) fn new( + caps: Caps, code: Code<'a>, main: Chunk, functions: Vec, @@ -331,6 +335,7 @@ impl<'a> Program<'a> { }); let mut program = Self { + caps, code, main, functions, @@ -363,6 +368,7 @@ impl<'a> Program<'a> { pub fn into_owned(self) -> Program<'static> { Program { code: Code::Owned(self.code.into_owned()), + caps: self.caps, main: self.main, functions: self.functions, max_stack: self.max_stack, @@ -401,7 +407,7 @@ impl<'a> Program<'a> { /// Cheap enough to run on every compile, and the gate an artifact loaded /// from a wire has to pass before the VM will touch it. pub fn verify(&self) -> Result, crate::grain::bytecode::VerifyError> { - crate::grain::bytecode::verify(&self.code, &self.chunks(), self.pools()) + crate::grain::bytecode::verify(self.caps, &self.code, &self.chunks(), &self.pools()) } /// Every chunk, main first, in the order they sit in the code. @@ -450,6 +456,12 @@ impl<'a> Program<'a> { &self.code } + /// The capabilities required by every chunk's instructions. + #[must_use] + pub fn caps(&self) -> Caps { + self.caps + } + /// The compiled script functions. #[must_use] pub fn functions(&self) -> &[Function] { @@ -794,6 +806,7 @@ mod tests { .collect(); Program::new( + Caps::empty(), code.into(), whole, functions, diff --git a/src/grain/vm/mod.rs b/src/grain/vm/mod.rs index 9d9c9b0f0..edd4cc538 100644 --- a/src/grain/vm/mod.rs +++ b/src/grain/vm/mod.rs @@ -4310,6 +4310,7 @@ impl<'e> Vm<'e> { mod tests { use super::*; use crate::grain::bytecode::{assemble, Chain, Chunk, Op, Positions, Step, Strings, Tail}; + use crate::grain::format::Abi; use crate::grain::program::{Function, Parts}; use crate::{CallFnOptions, Engine, Scope, INT}; @@ -4373,6 +4374,7 @@ mod tests { .collect(); Program::new( + Abi::host().caps, code.into(), Chunk::new(0, end_of(2), 8), functions, diff --git a/tests/grain/fixtures/golden.rgrn b/tests/grain/fixtures/golden.rgrn index 7ad782436..35c087616 100644 Binary files a/tests/grain/fixtures/golden.rgrn and b/tests/grain/fixtures/golden.rgrn differ diff --git a/tests/grain/format.rs b/tests/grain/format.rs index e6def1825..483a50327 100644 --- a/tests/grain/format.rs +++ b/tests/grain/format.rs @@ -10,7 +10,7 @@ use super::corpus; -use rhai::grain::format::{ReadError, WriteError}; +use rhai::grain::format::{Abi, ReadError, WriteError}; use rhai::grain::{Compiler, Program, Vm}; use rhai::{Dynamic, Engine, Scope, INT}; @@ -186,7 +186,7 @@ fn a_golden_artifact_written_by_an_older_build_still_runs() { // the fixture was not written for, this would test the guard rather than // the encoding. if !GOLDEN_APPLIES { - println!("skipped: the golden source uses syntax this build does not have"); + println!("skipped: the golden Rhai source requires features this build does not have to compile"); return; } @@ -204,25 +204,31 @@ fn a_golden_artifact_written_by_an_older_build_still_runs() { return; } + println!("host caps: {:?}", Abi::host()); + let bytes = std::fs::read(GOLDEN_ARTIFACT).expect("the golden artifact is checked in"); + let loaded = match Program::read(&bytes) { Ok(loaded) => loaded, // The header records the ABI the fixture was written under, and a build - // with different numeric widths or restriction flags refuses it *by - // design* — that refusal is what `abi.rs` is for. The fixture is one - // build's bytes, so it can only be checked on that build; anywhere else - // this would be testing the ABI guard rather than the encoding. - Err(err) if format!("{err}").contains("written with") => { - println!("skipped: the golden fixture is a default-build artifact ({err})"); + // with different numeric widths or capability flags refuses it *by design* + // — that refusal is what `abi.rs` is for. The fixture is the bytes of the + // default build, so it can only be checked on a compatible build; anywhere + // else this would be testing the ABI guard rather than the encoding. + Err(err) if format!("{err}").contains("artifact cannot load") => { + println!("skipped: the golden fixture is a default-build artifact and cannot load ({err})"); return; } Err(err) => panic!( - "the golden artifact no longer loads: {err}\n\ - The format moved. If that was deliberate, regenerate the fixture with \ - `REGENERATE_GOLDEN=1 cargo test --features grain --test grain golden`.", + "the golden artifact no longer loads:\n{err}\n\n\ + The format probably has moved.\n\ + If that was deliberate, regenerate the fixture with:\n\ + REGENERATE_GOLDEN=1 cargo test --features grain --test grain golden", ), }; + println!("artifact requires caps: {:?}", loaded.caps()); + // A fixture only pins what it contains, and narrowing one while editing the // source is easy and silent. These are read off the *artifact*, so they say // what the encoder branch coverage actually is rather than what the source @@ -505,6 +511,7 @@ fn a_future_format_version_is_refused_rather_than_guessed_at() { /// The fingerprint is the difference between a clean failure and integers /// decoded as the wrong type, so the error must name the flag. #[test] +#[cfg(not(feature = "no_index"))] fn a_different_value_representation_is_refused_by_name() { let engine = corpus::engine(); @@ -514,12 +521,16 @@ fn a_different_value_representation_is_refused_by_name() { let half = narrow[6] / 2; // INT width narrow[6] = half; let message = Program::read(&narrow).unwrap_err().to_string(); - assert!(message.contains("INT") && message.contains(&half.to_string()), "the message must name the width: {message}",); + assert!(message.contains("integer") && message.contains(&half.to_string()), "the message must name the width: {message}",); let mut restricted = sample(&engine); - restricted[8] ^= 0b100; // the `no_index` bit + let bits: [u8; 4] = restricted[8..12].try_into().expect("the caps is 4 bytes"); + let mut caps = rhai::grain::format::Caps::from_bits_retain(u32::from_le_bytes(bits)); + // Remove the indexing caps. + caps -= rhai::grain::format::Caps::INDEXING; + restricted[8..12].copy_from_slice(&caps.bits().to_le_bytes()); let message = Program::read(&restricted).unwrap_err().to_string(); - assert!(message.contains("no_index"), "the message must name the flag: {message}",); + assert!(message.contains("indexing"), "the message must name `indexing`: {message}",); } /// A `switch` carries hashes Rhai's parser computed, and Rhai seeds its hasher