use fix_error::Error; use fix_lang::StringId; use fix_runtime::{Machine, MachineExt, NixType, resolve_operand}; use gc_arena::{Gc, RefLock}; use smallvec::SmallVec; use crate::{ AttrSet, BytecodeReader, List, Step, StrictValue, Value, VmRuntimeCtx, VmRuntimeCtxExt, }; #[inline(always)] pub(crate) fn op_make_attrs<'gc, M: Machine<'gc>>( m: &mut M, ctx: &mut impl VmRuntimeCtx, reader: &mut BytecodeReader<'_>, mc: &gc_arena::Mutation<'gc>, ) -> Step { let static_count = reader.read_u32() as usize; let dynamic_count = reader.read_u32() as usize; for i in 0..dynamic_count { let depth = dynamic_count - 1 - i; m.force_slot_to_pc(depth, reader, mc, reader.inst_start_pc())?; } let mut dyn_keys: SmallVec<[_; 2]> = SmallVec::with_capacity(dynamic_count); for i in 0..dynamic_count { let depth = dynamic_count - 1 - i; let key_val = m.peek_forced(depth); let key_sid = match ctx.get_string_id(key_val) { Ok(id) => Some(id), Err(NixType::Null) => None, Err(got) => return m.finish_type_err(NixType::String, got), }; dyn_keys.push(key_sid); } m.drop_n(dynamic_count); let mut kv: SmallVec<[(crate::StringId, Value); 4]> = SmallVec::with_capacity(static_count + dynamic_count); for _ in 0..static_count { let key = reader.read_string_id(); let val = resolve_operand(&reader.read_operand_data(), mc, ctx, m); let _span_id = reader.read_u32(); kv.push((key, val)); } for key in dyn_keys { let val = resolve_operand(&reader.read_operand_data(), mc, ctx, m); let _span_id = reader.read_u32(); if let Some(key) = key { kv.push((key, val)) } } kv.sort_by_key(|(k, _)| *k); let attrs = Gc::new(mc, AttrSet::from_sorted_unchecked(kv)); m.push(Value::new(attrs)); Step::Continue(()) } #[inline(always)] pub(crate) fn op_make_empty_attrs<'gc, M: Machine<'gc>>(m: &mut M) -> Step { m.push(m.empty_attrs()); Step::Continue(()) } #[inline(always)] pub(crate) fn op_select_static<'gc, M: Machine<'gc>>( m: &mut M, ctx: &mut impl VmRuntimeCtx, reader: &mut BytecodeReader<'_>, mc: &gc_arena::Mutation<'gc>, ) -> Step { let _span_id = reader.read_u32(); let key = reader.read_string_id(); let attrset = m.force_and_retry::>(reader, mc)?; match attrset.lookup(key) { Some(v) => { m.push(v); } None => return select_skip(m, key, ctx, reader), } Step::Continue(()) } #[inline(always)] pub(crate) fn op_select_dynamic<'gc, M: Machine<'gc>>( m: &mut M, ctx: &mut impl VmRuntimeCtx, reader: &mut BytecodeReader<'_>, mc: &gc_arena::Mutation<'gc>, ) -> Step { let _span_id = reader.read_u32(); let (attrset, key_val) = m.force_and_retry::<(Gc, StrictValue)>(reader, mc)?; let key_sid = match ctx.get_string_id(key_val) { Ok(id) => id, Err(got) => return m.finish_type_err(NixType::String, got), }; match attrset.lookup(key_sid) { Some(v) => { m.push(v); } None => return select_skip(m, key_sid, ctx, reader), } Step::Continue(()) } /// Skip the rest of a **Select** attrpath after a missing attribute. /// Only recognises Select opcodes and jumps; encountering any other /// opcode means we've reached the end of the select sequence and /// should report the missing-attribute error. #[expect( clippy::cast_sign_loss, reason = "jump target = pc + offset is a non-negative bytecode address by codegen invariant" )] fn select_skip<'gc, M: Machine<'gc>>( m: &mut M, key: StringId, ctx: &mut impl VmRuntimeCtx, reader: &mut BytecodeReader<'_>, ) -> Step { use fix_bytecode::Op::*; loop { match reader.read_op() { // Skip rest of the attrpath SelectStatic => { reader.set_pc(reader.pc() + 4 + 4); } SelectDynamic => { reader.set_pc(reader.pc() + 4); } JumpIfSelectSucceeded => { reader.set_pc(reader.pc() + 4); break Step::Continue(()); } // Default (`a.b or c`) JumpIfSelectFailed => { let offset = reader.read_i32(); reader.set_pc(((reader.pc() as isize) + (offset as isize)) as usize); } // Report error _ => { let name = ctx.resolve_string(key); return m.finish_err(Error::eval_error(format!("attribute '{name}' missing"))); } } } } /// Skip the rest of a **HasAttr** attrpath after an intermediate /// lookup failed. Only recognises HasAttr opcodes and jumps. #[expect( clippy::cast_sign_loss, reason = "jump target = pc + offset is a non-negative bytecode address by codegen invariant" )] #[expect( clippy::unreachable, reason = "has_attr_skip only runs over a codegen-produced HasAttr attrpath sequence; any other opcode is a codegen bug" )] fn has_attr_skip(reader: &mut BytecodeReader<'_>) -> Step { use fix_bytecode::Op::*; loop { match reader.read_op() { HasAttrPathStatic => { reader.set_pc(reader.pc() + 4 + 4); } HasAttrPathDynamic => { reader.set_pc(reader.pc() + 4); } HasAttrStatic => { reader.set_pc(reader.pc() + 4); break Step::Continue(()); } JumpIfSelectFailed => { let offset = reader.read_i32(); reader.set_pc(((reader.pc() as isize) + (offset as isize)) as usize); } HasAttrDynamic => { break Step::Continue(()); } HasAttrResolve => { reader.set_pc(reader.pc() - 1); break Step::Continue(()); } other => { unreachable!("unexpected opcode {:?} in has_attr_skip", other as u8) } } } } #[inline(always)] pub(crate) fn op_has_attr_path_static<'gc, M: Machine<'gc>>( m: &mut M, _ctx: &mut impl VmRuntimeCtx, reader: &mut BytecodeReader<'_>, mc: &gc_arena::Mutation<'gc>, ) -> Step { let _span_id = reader.read_u32(); let key = reader.read_string_id(); let current = m.force_and_retry::(reader, mc)?; match current .downcast::() .and_then(|attrs| attrs.lookup(key)) { Some(v) => { m.push(v); } None => return has_attr_skip(reader), } Step::Continue(()) } #[inline(always)] pub(crate) fn op_has_attr_path_dynamic<'gc, M: Machine<'gc>>( m: &mut M, ctx: &mut impl VmRuntimeCtx, reader: &mut BytecodeReader<'_>, mc: &gc_arena::Mutation<'gc>, ) -> Step { let _span_id = reader.read_u32(); let (current, key_val) = m.force_and_retry::<(StrictValue, StrictValue)>(reader, mc)?; let key_sid = match ctx.get_string_id(key_val) { Ok(id) => id, Err(got) => return m.finish_type_err(NixType::String, got), }; match current .downcast::() .and_then(|attrs| attrs.lookup(key_sid)) { Some(v) => { m.push(v); } None => return has_attr_skip(reader), } Step::Continue(()) } #[inline(always)] pub(crate) fn op_jump_if_select_failed<'gc, M: Machine<'gc>>( _m: &mut M, reader: &mut BytecodeReader<'_>, ) -> Step { // No-op let _offset = reader.read_i32(); Step::Continue(()) } #[inline(always)] #[expect( clippy::cast_sign_loss, reason = "jump target = pc + offset is a non-negative bytecode address by codegen invariant" )] pub(crate) fn op_jump_if_select_succeeded<'gc, M: Machine<'gc>>( _m: &mut M, reader: &mut BytecodeReader<'_>, ) -> Step { let offset = reader.read_i32(); reader.set_pc(((reader.pc() as isize) + (offset as isize)) as usize); Step::Continue(()) } #[inline(always)] pub(crate) fn op_has_attr_static<'gc, M: Machine<'gc>>( m: &mut M, reader: &mut BytecodeReader<'_>, mc: &gc_arena::Mutation<'gc>, ) -> Step { let key = reader.read_string_id(); let current = m.force_and_retry::(reader, mc)?; m.push(Value::new( current .downcast::() .and_then(|attrs| attrs.lookup(key)) .is_some(), )); // Skip HasAttrResolve reader.set_pc(reader.pc() + 1); Step::Continue(()) } #[inline(always)] pub(crate) fn op_has_attr_dynamic<'gc, M: MachineExt<'gc>>( m: &mut M, ctx: &mut impl VmRuntimeCtx, reader: &mut BytecodeReader<'_>, mc: &gc_arena::Mutation<'gc>, ) -> Step { let (current, dyn_key) = m.force_and_retry::<(StrictValue, StrictValue)>(reader, mc)?; let key_sid = match ctx.get_string_id(dyn_key) { Ok(id) => id, Err(got) => return m.finish_type_err(NixType::String, got), }; m.push(Value::new( current .downcast::() .and_then(|attrs| attrs.lookup(key_sid)) .is_some(), )); // Skip HasAttrResolve reader.set_pc(reader.pc() + 1); Step::Continue(()) } #[inline(always)] pub(crate) fn op_has_attr_resolve<'gc, M: Machine<'gc>>(m: &mut M) -> Step { // If we reach here, has_attr check has failed, push false (AttrSet is already popped) m.push(Value::new(false)); Step::Continue(()) } #[inline(always)] pub(crate) fn op_make_list<'gc, M: Machine<'gc>>( m: &mut M, ctx: &mut impl VmRuntimeCtx, reader: &mut BytecodeReader<'_>, mc: &gc_arena::Mutation<'gc>, ) -> Step { let count = reader.read_u32() as usize; let mut items: SmallVec<[Value; 4]> = SmallVec::with_capacity(count); for _ in 0..count { items.push(resolve_operand(&reader.read_operand_data(), mc, ctx, m)); } let list = Gc::new( mc, List { inner: RefLock::new(items), }, ); m.push(Value::new(list)); Step::Continue(()) } #[inline(always)] pub(crate) fn op_make_empty_list<'gc, M: Machine<'gc>>(m: &mut M) -> Step { m.push(m.empty_list()); Step::Continue(()) }