Compare commits

...
10 Commits
68 changed files with 3597 additions and 1924 deletions
Generated
+313 -349
View File
File diff suppressed because it is too large Load Diff
+68 -7
View File
@@ -6,6 +6,7 @@ members = [
"fix-compiler", "fix-compiler",
"fix-error", "fix-error",
"fix-lang", "fix-lang",
"fix-macros",
"fix-runtime", "fix-runtime",
"fix-vm", "fix-vm",
] ]
@@ -21,17 +22,77 @@ bumpalo = {
} }
ere = "0.2" ere = "0.2"
ghost-cell = "0.2" ghost-cell = "0.2"
hashbrown = "0.16" hashbrown = "0.17"
num_enum = "0.7.5" num_enum = "0.7"
rnix = "0.14" rnix = "0.14"
rowan = "0.16" rowan = "0.16"
smallvec = { version = "1.15", features = ["const_generics", "const_new"] } smallvec = { version = "1.15", features = ["const_generics", "const_new"] }
string-interner = "0.19" string-interner = "0.20"
gc-arena = { version = "0.7", features = ["hashbrown", "smallvec"] }
[workspace.dependencies.gc-arena] # https://emschwartz.me/your-clippy-config-should-be-stricter/
git = "https://github.com/kyren/gc-arena" # tombi: format.rules.table-keys-order.disabled = true
rev = "75671ae03f53718357b741ed4027560f14e90836" [workspace.lints.clippy]
features = ["allocator-api2", "hashbrown", "smallvec"] # Don't Panic - prevent panics from unwraps and unsafe slicing or indexing
string_slice = "warn"
indexing_slicing = "warn"
unwrap_used = "warn"
panic = "warn"
# WIP evaluator: several features are deliberately stubbed with todo!/unimplemented!
todo = "allow"
unimplemented = "allow"
unreachable = "warn"
get_unwrap = "warn"
unwrap_in_result = "warn"
unchecked_time_subtraction = "warn"
panic_in_result_fn = "warn"
# Optional - see post for caveats
# expect_used = "warn"
# arithmetic_side_effects = "warn"
# Don't Fail Silently - prevent dropped futures and swallowed errors
let_underscore_future = "warn"
let_underscore_must_use = "warn"
unused_result_ok = "warn"
map_err_ignore = "warn"
assertions_on_result_states = "warn"
# Don't Do Bad Async Stuff - prevent deadlocks and concurrency bugs
await_holding_lock = "warn"
await_holding_refcell_ref = "warn"
if_let_mutex = "warn" # only relevant on editions before 2024
large_futures = "warn"
# Don't Do Unsafe Things with Memory
mem_forget = "warn"
undocumented_unsafe_blocks = "warn"
multiple_unsafe_ops_per_block = "warn"
unnecessary_safety_doc = "warn"
unnecessary_safety_comment = "warn"
# Don't Do Potentially Incorrect Things with Numbers
float_cmp = "warn"
float_cmp_const = "warn"
lossy_float_literal = "warn"
cast_sign_loss = "warn"
invalid_upcast_comparisons = "warn"
# Optional - these effectively force you to document numeric invariants
# cast_possible_wrap = "warn"
# cast_precision_loss = "warn"
# cast_possible_truncation = "warn"
# Don't Do Bad Things That are Easy to Avoid
rc_mutex = "warn"
debug_assert_with_mut_call = "warn"
iter_not_returning_iterator = "warn"
expl_impl_clone_on_copy = "warn"
infallible_try_from = "warn"
dbg_macro = "warn"
# Don't `allow` Your Way Around These Lints - every suppression must be
# a deliberate #[expect(..., reason = "…")] rather than a silent #[allow]
allow_attributes = "warn"
allow_attributes_without_reason = "warn"
[profile.lto] [profile.lto]
inherits = "release" inherits = "release"
-16
View File
@@ -14,22 +14,6 @@
@evalr expr: @evalr expr:
cargo run --release -- eval --expr '{{expr}}' cargo run --release -- eval --expr '{{expr}}'
[no-exit-message]
@repli:
cargo run --release --features inspector -- --inspect-brk 127.0.0.1:9229 repl
[no-exit-message]
@evali expr:
cargo run --release --features inspector -- --inspect-brk 127.0.0.1:9229 eval --expr '{{expr}}'
[no-exit-message]
@replp:
cargo run --release --features prof -- repl
[no-exit-message]
@evalp expr:
cargo run --release --features prof -- eval --expr '{{expr}}'
[no-exit-message] [no-exit-message]
[positional-arguments] [positional-arguments]
@cg *args='': @cg *args='':
+5
View File
@@ -0,0 +1,5 @@
allow-indexing-slicing-in-tests = true
allow-panic-in-tests = true
allow-unwrap-in-tests = true
allow-expect-in-tests = true
allow-dbg-in-tests = true
+24
View File
@@ -0,0 +1,24 @@
{ pkgs }:
pkgs.mkShell {
packages = with pkgs; [
(fenix.latest.withComponents [
"cargo"
"clippy"
"rust-src"
"rustc"
"rustfmt"
"rust-analyzer"
])
cargo-machete
cargo-bloat
cargo-expand
lldb
valgrind
kdePackages.kcachegrind
hyperfine
just
samply
tokei
tombi
];
}
+3
View File
@@ -9,3 +9,6 @@ num_enum = { workspace = true }
string-interner = { workspace = true } string-interner = { workspace = true }
fix-lang = { path = "../fix-lang" } fix-lang = { path = "../fix-lang" }
[lints]
workspace = true
+79 -152
View File
@@ -2,7 +2,7 @@ use std::fmt::Write;
use colored::Colorize as _; use colored::Colorize as _;
use crate::{InstructionPtr, Op, OperandType, Continuation}; use crate::{BytecodeReader, Continuation, InstructionPtr, Op};
pub trait DisassemblerContext { pub trait DisassemblerContext {
fn resolve_string(&self, id: u32) -> &str; fn resolve_string(&self, id: u32) -> &str;
@@ -10,99 +10,15 @@ pub trait DisassemblerContext {
} }
pub struct Disassembler<'a, Ctx> { pub struct Disassembler<'a, Ctx> {
code: &'a [u8], reader: BytecodeReader<'a>,
ctx: &'a Ctx, ctx: &'a Ctx,
pc: usize,
} }
impl<'a, Ctx: DisassemblerContext> Disassembler<'a, Ctx> { impl<'a, Ctx: DisassemblerContext> Disassembler<'a, Ctx> {
pub fn new(ip: InstructionPtr, ctx: &'a Ctx) -> Self { pub fn new(ip: InstructionPtr, ctx: &'a Ctx) -> Self {
Self { Self {
code: ctx.get_code(), reader: BytecodeReader::new(ctx.get_code(), ip.0),
ctx, ctx,
pc: ip.0,
}
}
#[inline(always)]
fn read_u8(&mut self) -> u8 {
let b = self.code[self.pc];
self.pc += 1;
b
}
#[inline(always)]
fn read_u16(&mut self) -> u16 {
let bytes = self.code[self.pc..self.pc + 2]
.try_into()
.expect("no enough bytes");
self.pc += 2;
u16::from_le_bytes(bytes)
}
#[inline(always)]
fn read_u32(&mut self) -> u32 {
let bytes = self.code[self.pc..self.pc + 4]
.try_into()
.expect("no enough bytes");
self.pc += 4;
u32::from_le_bytes(bytes)
}
#[inline(always)]
fn read_i32(&mut self) -> i32 {
let bytes = self.code[self.pc..self.pc + 4]
.try_into()
.expect("no enough bytes");
self.pc += 4;
i32::from_le_bytes(bytes)
}
#[inline(always)]
fn read_i64(&mut self) -> i64 {
let bytes = self.code[self.pc..self.pc + 8]
.try_into()
.expect("no enough bytes");
self.pc += 8;
i64::from_le_bytes(bytes)
}
#[inline(always)]
fn read_f64(&mut self) -> f64 {
let bytes = self.code[self.pc..self.pc + 8]
.try_into()
.expect("no enough bytes");
self.pc += 8;
f64::from_le_bytes(bytes)
}
#[inline(always)]
fn read_operand_data(&mut self) {
use OperandType::*;
let tag = self.read_u8();
let ty = OperandType::try_from(tag).expect("invalid operand type");
match ty {
Const => {
self.read_u32();
}
BigInt => {
self.read_i64();
}
Local => {
self.read_u8();
self.read_u32();
}
BuiltinConst => {
self.read_u32();
}
Builtins => {}
ReplBinding => {
self.read_u32();
}
ScopedImportBinding => {
self.read_u32();
self.read_u32();
}
} }
} }
@@ -114,6 +30,11 @@ impl<'a, Ctx: DisassemblerContext> Disassembler<'a, Ctx> {
self.disassemble_impl(true) self.disassemble_impl(true)
} }
#[expect(
clippy::let_underscore_must_use,
clippy::indexing_slicing,
reason = "disassembler operates on well-formed bytecode; writes target an in-memory String via fmt::Write, which is infallible"
)]
fn disassemble_impl(&mut self, color: bool) -> String { fn disassemble_impl(&mut self, color: bool) -> String {
let mut out = String::new(); let mut out = String::new();
if color { if color {
@@ -122,24 +43,24 @@ impl<'a, Ctx: DisassemblerContext> Disassembler<'a, Ctx> {
out, out,
"{} {}", "{} {}",
"Length:".white(), "Length:".white(),
format!("{} bytes", self.code.len()).cyan() format!("{} bytes", self.reader.len()).cyan()
); );
} else { } else {
let _ = writeln!(out, "=== Bytecode Disassembly ==="); let _ = writeln!(out, "=== Bytecode Disassembly ===");
let _ = writeln!(out, "Length: {} bytes", self.code.len()); let _ = writeln!(out, "Length: {} bytes", self.reader.len());
} }
while self.pc < self.code.len() { while self.reader.pc() < self.reader.len() {
let start_pos = self.pc; let start_pos = self.reader.pc();
let op_byte = self.read_u8(); let op_byte = self.reader.read_u8();
let (mnemonic, args) = self.decode_instruction(op_byte, start_pos); let (mnemonic, args) = self.decode_instruction(op_byte, start_pos);
let bytes_slice = &self.code[start_pos + 1..self.pc]; let bytes_slice = &self.reader[start_pos + 1..self.reader.pc()];
let mut chunks = bytes_slice.chunks(4); let mut chunks = bytes_slice.chunks(4);
let first_chunk = chunks.next().unwrap_or(&[]); let first_chunk = chunks.next().unwrap_or(&[]);
let bytes_str = { let bytes_str = {
let mut temp = format!("{:02x}", self.code[start_pos]); let mut temp = format!("{:02x}", self.reader[start_pos]);
for b in first_chunk { for b in first_chunk {
let _ = write!(&mut temp, " {:02x}", b); let _ = write!(&mut temp, " {:02x}", b);
} }
@@ -199,31 +120,36 @@ impl<'a, Ctx: DisassemblerContext> Disassembler<'a, Ctx> {
out out
} }
#[expect(
clippy::let_underscore_must_use,
clippy::cast_sign_loss,
reason = "disassembler operates on well-formed bytecode; jump targets are valid non-negative PCs and writes to String are infallible"
)]
fn decode_instruction(&mut self, op_byte: u8, current_pc: usize) -> (&'static str, String) { fn decode_instruction(&mut self, op_byte: u8, current_pc: usize) -> (&'static str, String) {
let op = Op::try_from(op_byte).expect("invalid op code"); let op = Op::try_from(op_byte).expect("invalid op code");
match op { match op {
Op::PushSmi => { Op::PushSmi => {
let val = self.read_i32(); let val = self.reader.read_i32();
("PushSmi", format!("{}", val)) ("PushSmi", format!("{}", val))
} }
Op::PushBigInt => { Op::PushBigInt => {
let val = self.read_i64(); let val = self.reader.read_i64();
("PushBigInt", format!("{}", val)) ("PushBigInt", format!("{}", val))
} }
Op::PushFloat => { Op::PushFloat => {
let val = self.read_f64(); let val = self.reader.read_f64();
("PushFloat", format!("{}", val)) ("PushFloat", format!("{}", val))
} }
Op::PushString => { Op::PushString => {
let idx = self.read_u32(); let idx = self.reader.read_u32();
let s = self.ctx.resolve_string(idx); let s = self.ctx.resolve_string(idx);
let len = s.len(); let len = s.len();
let mut s_fmt = format!("{:?}", s); let mut s_fmt = format!("{:?}", s);
if s_fmt.len() > 60 { if s_fmt.len() > 60 {
s_fmt.truncate(57); s_fmt.truncate(57);
#[allow(clippy::unwrap_used)] write!(s_fmt, "...\" (total {len} bytes)")
write!(s_fmt, "...\" (total {len} bytes)").unwrap(); .expect("writing to String is infallible");
} }
("PushString", format!("@{} {}", idx, s_fmt)) ("PushString", format!("@{} {}", idx, s_fmt))
} }
@@ -232,38 +158,38 @@ impl<'a, Ctx: DisassemblerContext> Disassembler<'a, Ctx> {
Op::PushFalse => ("PushFalse", String::new()), Op::PushFalse => ("PushFalse", String::new()),
Op::LoadLocal => { Op::LoadLocal => {
let idx = self.read_u32(); let idx = self.reader.read_u32();
("LoadLocal", format!("[{}]", idx)) ("LoadLocal", format!("[{}]", idx))
} }
Op::LoadOuter => { Op::LoadOuter => {
let depth = self.read_u8(); let depth = self.reader.read_u8();
let idx = self.read_u32(); let idx = self.reader.read_u32();
("LoadOuter", format!("depth={} [{}]", depth, idx)) ("LoadOuter", format!("depth={} [{}]", depth, idx))
} }
Op::StoreLocal => { Op::StoreLocal => {
let idx = self.read_u32(); let idx = self.reader.read_u32();
("StoreLocal", format!("[{}]", idx)) ("StoreLocal", format!("[{}]", idx))
} }
Op::AllocLocals => { Op::AllocLocals => {
let count = self.read_u32(); let count = self.reader.read_u32();
("AllocLocals", format!("count={}", count)) ("AllocLocals", format!("count={}", count))
} }
Op::MakeThunk => { Op::MakeThunk => {
let offset = self.read_u32(); let offset = self.reader.read_u32();
("MakeThunk", format!("-> {:04x}", offset)) ("MakeThunk", format!("-> {:04x}", offset))
} }
Op::MakeClosure => { Op::MakeClosure => {
let offset = self.read_u32(); let offset = self.reader.read_u32();
let slots = self.read_u32(); let slots = self.reader.read_u32();
("MakeClosure", format!("-> {:04x} slots={}", offset, slots)) ("MakeClosure", format!("-> {:04x} slots={}", offset, slots))
} }
Op::MakePatternClosure => { Op::MakePatternClosure => {
let offset = self.read_u32(); let offset = self.reader.read_u32();
let slots = self.read_u32(); let slots = self.reader.read_u32();
let req_count = self.read_u16(); let req_count = self.reader.read_u16();
let opt_count = self.read_u16(); let opt_count = self.reader.read_u16();
let ellipsis = self.read_u8() != 0; let ellipsis = self.reader.read_u8() != 0;
let mut arg_str = format!( let mut arg_str = format!(
"-> {:04x} slots={} req={} opt={} ...={})", "-> {:04x} slots={} req={} opt={} ...={})",
@@ -272,18 +198,18 @@ impl<'a, Ctx: DisassemblerContext> Disassembler<'a, Ctx> {
arg_str.push_str(" Args=["); arg_str.push_str(" Args=[");
for _ in 0..req_count { for _ in 0..req_count {
let idx = self.read_u32(); let idx = self.reader.read_u32();
arg_str.push_str(&format!("Req({}) ", self.ctx.resolve_string(idx))); arg_str.push_str(&format!("Req({}) ", self.ctx.resolve_string(idx)));
} }
for _ in 0..opt_count { for _ in 0..opt_count {
let idx = self.read_u32(); let idx = self.reader.read_u32();
arg_str.push_str(&format!("Opt({}) ", self.ctx.resolve_string(idx))); arg_str.push_str(&format!("Opt({}) ", self.ctx.resolve_string(idx)));
} }
let total_args = req_count + opt_count; let total_args = req_count + opt_count;
for _ in 0..total_args { for _ in 0..total_args {
let _name_idx = self.read_u32(); let _name_idx = self.reader.read_u32();
let _span_id = self.read_u32(); let _span_id = self.reader.read_u32();
} }
arg_str.push(']'); arg_str.push(']');
@@ -291,31 +217,32 @@ impl<'a, Ctx: DisassemblerContext> Disassembler<'a, Ctx> {
} }
Op::Call => { Op::Call => {
self.read_operand_data(); let _ = self.reader.read_operand_data();
("Call", "arg=?".into()) ("Call", "arg=?".into())
} }
Op::DispatchCont => { Op::DispatchCont => {
let phase = Continuation::try_from(self.read_u8()).expect("invalid primop phase"); let phase =
Continuation::try_from(self.reader.read_u8()).expect("invalid primop phase");
("DispatchPrimOp", format!("phase={phase:?}")) ("DispatchPrimOp", format!("phase={phase:?}"))
} }
Op::MakeAttrs => { Op::MakeAttrs => {
let static_count = self.read_u32(); let static_count = self.reader.read_u32();
let dynamic_count = self.read_u32(); let dynamic_count = self.reader.read_u32();
let mut args = format!("static={} dynamic={}", static_count, dynamic_count); let mut args = format!("static={} dynamic={}", static_count, dynamic_count);
for _ in 0..static_count { for _ in 0..static_count {
let key_id = self.read_u32(); let key_id = self.reader.read_u32();
let _ = write!(args, " [{}={}", self.ctx.resolve_string(key_id), key_id); let _ = write!(args, " [{}={}", self.ctx.resolve_string(key_id), key_id);
self.read_operand_data(); let _ = self.reader.read_operand_data();
let _span_id = self.read_u32(); let _span_id = self.reader.read_u32();
args.push(']'); args.push(']');
} }
for _ in 0..dynamic_count { for _ in 0..dynamic_count {
let _ = write!(args, " [dyn"); let _ = write!(args, " [dyn");
self.read_operand_data(); let _ = self.reader.read_operand_data();
let _span_id = self.read_u32(); let _span_id = self.reader.read_u32();
args.push(']'); args.push(']');
} }
@@ -324,31 +251,31 @@ impl<'a, Ctx: DisassemblerContext> Disassembler<'a, Ctx> {
Op::MakeEmptyAttrs => ("MakeEmptyAttrs", String::new()), Op::MakeEmptyAttrs => ("MakeEmptyAttrs", String::new()),
Op::SelectStatic => { Op::SelectStatic => {
let span_id = self.read_u32(); let span_id = self.reader.read_u32();
let key_id = self.read_u32(); let key_id = self.reader.read_u32();
( (
"SelectStatic", "SelectStatic",
format!("key={} span={}", self.ctx.resolve_string(key_id), span_id), format!("key={} span={}", self.ctx.resolve_string(key_id), span_id),
) )
} }
Op::SelectDynamic => { Op::SelectDynamic => {
let span_id = self.read_u32(); let span_id = self.reader.read_u32();
("SelectDynamic", format!("span={}", span_id)) ("SelectDynamic", format!("span={}", span_id))
} }
Op::HasAttrPathStatic => { Op::HasAttrPathStatic => {
let span_id = self.read_u32(); let span_id = self.reader.read_u32();
let key_id = self.read_u32(); let key_id = self.reader.read_u32();
( (
"HasAttrPathStatic", "HasAttrPathStatic",
format!("key={} span={}", self.ctx.resolve_string(key_id), span_id), format!("key={} span={}", self.ctx.resolve_string(key_id), span_id),
) )
} }
Op::HasAttrPathDynamic => { Op::HasAttrPathDynamic => {
let span_id = self.read_u32(); let span_id = self.reader.read_u32();
("HasAttrPathDynamic", format!("span={}", span_id)) ("HasAttrPathDynamic", format!("span={}", span_id))
} }
Op::HasAttrStatic => { Op::HasAttrStatic => {
let key_id = self.read_u32(); let key_id = self.reader.read_u32();
( (
"HasAttrStatic", "HasAttrStatic",
format!("key={}", self.ctx.resolve_string(key_id)), format!("key={}", self.ctx.resolve_string(key_id)),
@@ -357,7 +284,7 @@ impl<'a, Ctx: DisassemblerContext> Disassembler<'a, Ctx> {
Op::HasAttrDynamic => ("HasAttrDynamic", String::new()), Op::HasAttrDynamic => ("HasAttrDynamic", String::new()),
Op::HasAttrResolve => ("HasAttrResolve", String::new()), Op::HasAttrResolve => ("HasAttrResolve", String::new()),
Op::JumpIfSelectSucceeded => { Op::JumpIfSelectSucceeded => {
let offset = self.read_i32(); let offset = self.reader.read_i32();
let target = (current_pc as isize + 1 + 4 + offset as isize) as usize; let target = (current_pc as isize + 1 + 4 + offset as isize) as usize;
( (
"JumpIfSelectSucceeded", "JumpIfSelectSucceeded",
@@ -365,7 +292,7 @@ impl<'a, Ctx: DisassemblerContext> Disassembler<'a, Ctx> {
) )
} }
Op::JumpIfSelectFailed => { Op::JumpIfSelectFailed => {
let offset = self.read_i32(); let offset = self.reader.read_i32();
let target = (current_pc as isize + 1 + 4 + offset as isize) as usize; let target = (current_pc as isize + 1 + 4 + offset as isize) as usize;
( (
"JumpIfSelectFailed", "JumpIfSelectFailed",
@@ -374,9 +301,9 @@ impl<'a, Ctx: DisassemblerContext> Disassembler<'a, Ctx> {
} }
Op::MakeList => { Op::MakeList => {
let count = self.read_u32(); let count = self.reader.read_u32();
for _ in 0..count { for _ in 0..count {
self.read_operand_data(); let _ = self.reader.read_operand_data();
} }
("MakeList", format!("size={}", count)) ("MakeList", format!("size={}", count))
} }
@@ -398,7 +325,7 @@ impl<'a, Ctx: DisassemblerContext> Disassembler<'a, Ctx> {
Op::OpNot => ("OpNot", String::new()), Op::OpNot => ("OpNot", String::new()),
Op::JumpIfFalse => { Op::JumpIfFalse => {
let offset = self.read_i32(); let offset = self.reader.read_i32();
let target = (current_pc as isize + 1 + 4 + offset as isize) as usize; let target = (current_pc as isize + 1 + 4 + offset as isize) as usize;
( (
"JumpIfFalse", "JumpIfFalse",
@@ -406,55 +333,55 @@ impl<'a, Ctx: DisassemblerContext> Disassembler<'a, Ctx> {
) )
} }
Op::JumpIfTrue => { Op::JumpIfTrue => {
let offset = self.read_i32(); let offset = self.reader.read_i32();
let target = (current_pc as isize + 1 + 4 + offset as isize) as usize; let target = (current_pc as isize + 1 + 4 + offset as isize) as usize;
("JumpIfTrue", format!("-> {:04x} offset={}", target, offset)) ("JumpIfTrue", format!("-> {:04x} offset={}", target, offset))
} }
Op::Jump => { Op::Jump => {
let offset = self.read_i32(); let offset = self.reader.read_i32();
let target = (current_pc as isize + 1 + 4 + offset as isize) as usize; let target = (current_pc as isize + 1 + 4 + offset as isize) as usize;
("Jump", format!("-> {:04x} offset={}", target, offset)) ("Jump", format!("-> {:04x} offset={}", target, offset))
} }
Op::ConcatStrings => { Op::ConcatStrings => {
let count = self.read_u16(); let count = self.reader.read_u16();
let force = self.read_u8(); let force = self.reader.read_u8();
("ConcatStrings", format!("count={} force={}", count, force)) ("ConcatStrings", format!("count={} force={}", count, force))
} }
Op::CoerceToString => ("CoerceToString", String::new()), Op::CoerceToString => ("CoerceToString", String::new()),
Op::ResolvePath => { Op::ResolvePath => {
let dir_id = self.read_u32(); let dir_id = self.reader.read_u32();
let dir = self.ctx.resolve_string(dir_id); let dir = self.ctx.resolve_string(dir_id);
("ResolvePath", format!("dir={:?}", dir)) ("ResolvePath", format!("dir={:?}", dir))
} }
Op::Assert => { Op::Assert => {
let raw_idx = self.read_u32(); let raw_idx = self.reader.read_u32();
let span_id = self.read_u32(); let span_id = self.reader.read_u32();
("Assert", format!("text_id={} span={}", raw_idx, span_id)) ("Assert", format!("text_id={} span={}", raw_idx, span_id))
} }
Op::LookupWith => { Op::LookupWith => {
let idx = self.read_u32(); let idx = self.reader.read_u32();
let name = self.ctx.resolve_string(idx); let name = self.ctx.resolve_string(idx);
let n = self.read_u8(); let n = self.reader.read_u8();
for _ in 0..n { for _ in 0..n {
self.read_operand_data(); let _ = self.reader.read_operand_data();
} }
("LookupWith", format!("sym={:?} n={}", name, n)) ("LookupWith", format!("sym={:?} n={}", name, n))
} }
Op::LoadBuiltins => ("LoadBuiltins", String::new()), Op::LoadBuiltins => ("LoadBuiltins", String::new()),
Op::LoadBuiltin => { Op::LoadBuiltin => {
let id = self.read_u8(); let id = self.reader.read_u8();
("LoadBuiltin", format!("id={}", id)) ("LoadBuiltin", format!("id={}", id))
} }
Op::LoadReplBinding => { Op::LoadReplBinding => {
let idx = self.read_u32(); let idx = self.reader.read_u32();
let name = self.ctx.resolve_string(idx); let name = self.ctx.resolve_string(idx);
("LoadReplBinding", format!("{:?}", name)) ("LoadReplBinding", format!("{:?}", name))
} }
Op::LoadScopedBinding => { Op::LoadScopedBinding => {
let slot = self.read_u32(); let slot = self.reader.read_u32();
let idx = self.read_u32(); let idx = self.reader.read_u32();
let name = self.ctx.resolve_string(idx); let name = self.ctx.resolve_string(idx);
("LoadScopedBinding", format!("slot={} {:?}", slot, name)) ("LoadScopedBinding", format!("slot={} {:?}", slot, name))
} }
+195 -114
View File
@@ -1,4 +1,7 @@
#![allow(dead_code)] #![allow(
dead_code,
reason = "crate is under active development; some opcodes and helpers are not yet wired up"
)]
use fix_lang::{BuiltinId, StringId}; use fix_lang::{BuiltinId, StringId};
use num_enum::TryFromPrimitive; use num_enum::TryFromPrimitive;
@@ -10,8 +13,7 @@ pub mod disassembler;
pub struct InstructionPtr(pub usize); pub struct InstructionPtr(pub usize);
#[repr(u8)] #[repr(u8)]
#[derive(Debug, Clone, Copy, TryFromPrimitive)] #[derive(Debug, Clone, Copy)]
#[allow(clippy::enum_variant_names)]
pub enum Op { pub enum Op {
PushSmi, PushSmi,
PushBigInt, PushBigInt,
@@ -88,6 +90,21 @@ pub enum Op {
Illegal, Illegal,
} }
impl TryFrom<u8> for Op {
type Error = u8;
#[inline(always)]
fn try_from(value: u8) -> Result<Self, Self::Error> {
if (0..Self::Illegal as u8).contains(&value) {
// SAFETY: `value` is in `0..Illegal`, which are exactly the valid
// discriminants of this `#[repr(u8)]` enum, so the transmute is sound.
Ok(unsafe { std::mem::transmute::<u8, Self>(value) })
} else {
Err(value)
}
}
}
#[repr(u8)] #[repr(u8)]
#[derive(Debug, Clone, Copy, TryFromPrimitive)] #[derive(Debug, Clone, Copy, TryFromPrimitive)]
pub enum OperandType { pub enum OperandType {
@@ -106,11 +123,7 @@ pub enum Const {
Bool(bool), Bool(bool),
String(StringId), String(StringId),
Path(StringId), Path(StringId),
PrimOp { PrimOp(BuiltinId),
id: BuiltinId,
arity: u8,
dispatch_ip: u32,
},
Null, Null,
} }
@@ -139,13 +152,15 @@ pub enum Continuation {
PAdd, PAdd,
PAddErrorContext, PAddErrorContext,
PAll, PAll0,
PAllCallPred, PAll1,
PAllCheck, PAll2,
PAll3,
PAny, PAny0,
PAnyCallPred, PAny1,
PAnyCheck, PAny2,
PAny3,
PAppendContext, PAppendContext,
PAppendContextLoop, PAppendContextLoop,
@@ -169,9 +184,12 @@ pub enum Continuation {
PConcatStringsSep, PConcatStringsSep,
PConvertHash, PConvertHash,
PDeepSeq, PDeepSeq0,
PDeepSeqPush, PDeepSeq1,
PDeepSeqLoop, PDeepSeq2,
PDeepSeq3,
PDeepSeq4,
PDeepSeq5,
PDerivation, PDerivation,
PDerivationStrict, PDerivationStrict,
@@ -185,18 +203,21 @@ pub enum Continuation {
PFetchTree, PFetchTree,
PFetchUrl, PFetchUrl,
PFilterForceList, PFilter0,
PFilterCallPred, PFilter1,
PFilterCheck, PFilter2,
PFilter3,
PFilter4,
PFilterSource, PFilterSource,
PFindFile, PFindFile,
PFloor, PFloor,
PFoldlStrict, PFoldlStrict0,
PFoldlStrictEmpty, PFoldlStrict1,
PFoldlStrictCall1, PFoldlStrict2,
PFoldlStrictCall2, PFoldlStrict3,
PFoldlStrictUpdate, PFoldlStrict4,
PFoldlStrict5,
PFromJSON, PFromJSON,
PFromTOML, PFromTOML,
PFunctionArgs, PFunctionArgs,
@@ -244,7 +265,8 @@ pub enum Continuation {
PReadFileType, PReadFileType,
PRemoveAttrs, PRemoveAttrs,
PReplaceStrings, PReplaceStrings,
PSeq, PSeq0,
PSeq1,
PSort, PSort,
PSplit, PSplit,
PSplitVersion, PSplitVersion,
@@ -286,8 +308,11 @@ pub enum Continuation {
impl TryFrom<u8> for Continuation { impl TryFrom<u8> for Continuation {
type Error = u8; type Error = u8;
#[inline(always)]
fn try_from(value: u8) -> Result<Self, Self::Error> { fn try_from(value: u8) -> Result<Self, Self::Error> {
if (0..Self::Illegal as u8).contains(&value) { if (0..Self::Illegal as u8).contains(&value) {
// SAFETY: `value` is in `0..Illegal`, which are exactly the valid
// discriminants of this `#[repr(u8)]` enum, so the transmute is sound.
Ok(unsafe { std::mem::transmute::<u8, Self>(value) }) Ok(unsafe { std::mem::transmute::<u8, Self>(value) })
} else { } else {
Err(value) Err(value)
@@ -296,14 +321,14 @@ impl TryFrom<u8> for Continuation {
} }
impl Continuation { impl Continuation {
pub fn entry_for_builtin(id: BuiltinId) -> Self { pub const fn entry_for_builtin(id: BuiltinId) -> Self {
use BuiltinId::*; use BuiltinId::*;
match id { match id {
Abort => Self::PAbort, Abort => Self::PAbort,
Add => Self::PAdd, Add => Self::PAdd,
AddErrorContext => Self::PAddErrorContext, AddErrorContext => Self::PAddErrorContext,
All => Self::PAll, All => Self::PAll0,
Any => Self::PAny, Any => Self::PAny0,
AppendContext => Self::PAppendContext, AppendContext => Self::PAppendContext,
AttrNames => Self::PAttrNames, AttrNames => Self::PAttrNames,
AttrValues => Self::PAttrValues, AttrValues => Self::PAttrValues,
@@ -319,7 +344,7 @@ impl Continuation {
ConcatMap => Self::PConcatMap, ConcatMap => Self::PConcatMap,
ConcatStringsSep => Self::PConcatStringsSep, ConcatStringsSep => Self::PConcatStringsSep,
ConvertHash => Self::PConvertHash, ConvertHash => Self::PConvertHash,
DeepSeq => Self::PDeepSeq, DeepSeq => Self::PDeepSeq0,
Derivation => Self::PDerivation, Derivation => Self::PDerivation,
DerivationStrict => Self::PDerivationStrict, DerivationStrict => Self::PDerivationStrict,
DirOf => Self::PDirOf, DirOf => Self::PDirOf,
@@ -331,11 +356,11 @@ impl Continuation {
FetchTarball => Self::PFetchTarball, FetchTarball => Self::PFetchTarball,
FetchTree => Self::PFetchTree, FetchTree => Self::PFetchTree,
FetchUrl => Self::PFetchUrl, FetchUrl => Self::PFetchUrl,
Filter => Self::PFilterForceList, Filter => Self::PFilter0,
FilterSource => Self::PFilterSource, FilterSource => Self::PFilterSource,
FindFile => Self::PFindFile, FindFile => Self::PFindFile,
Floor => Self::PFloor, Floor => Self::PFloor,
FoldlStrict => Self::PFoldlStrict, FoldlStrict => Self::PFoldlStrict0,
FromJSON => Self::PFromJSON, FromJSON => Self::PFromJSON,
FromTOML => Self::PFromTOML, FromTOML => Self::PFromTOML,
FunctionArgs => Self::PFunctionArgs, FunctionArgs => Self::PFunctionArgs,
@@ -379,7 +404,7 @@ impl Continuation {
RemoveAttrs => Self::PRemoveAttrs, RemoveAttrs => Self::PRemoveAttrs,
ReplaceStrings => Self::PReplaceStrings, ReplaceStrings => Self::PReplaceStrings,
ScopedImport => Self::PScopedImport, ScopedImport => Self::PScopedImport,
Seq => Self::PSeq, Seq => Self::PSeq0,
Sort => Self::PSort, Sort => Self::PSort,
Split => Self::PSplit, Split => Self::PSplit,
SplitVersion => Self::PSplitVersion, SplitVersion => Self::PSplitVersion,
@@ -405,7 +430,7 @@ impl Continuation {
} }
} }
pub fn ip(self) -> u32 { pub const fn ip(self) -> u32 {
self as u32 * 2 self as u32 * 2
} }
} }
@@ -416,7 +441,129 @@ pub struct BytecodeReader<'a> {
inst_start_pc: usize, inst_start_pc: usize,
} }
impl std::ops::Deref for BytecodeReader<'_> {
type Target = [u8];
fn deref(&self) -> &Self::Target {
self.bytecode
}
}
pub trait FromBytecode {
#[must_use]
fn read(reader: &mut BytecodeReader<'_>) -> Self;
}
impl<const N: usize> FromBytecode for [u8; N] {
#[inline(always)]
#[expect(
clippy::indexing_slicing,
reason = "reads well-formed bytecode; an out-of-range PC indicates a codegen bug and should panic loudly"
)]
fn read(reader: &mut BytecodeReader<'_>) -> Self {
let ret = reader.bytecode[reader.pc..reader.pc + N]
.try_into()
.expect("read_array failed");
reader.pc += N;
ret
}
}
impl FromBytecode for u8 {
#[inline(always)]
fn read(reader: &mut BytecodeReader<'_>) -> Self {
let [byte] = reader.read();
byte
}
}
macro_rules! numbers_impl {
( $($ty:ty),* ) => {
$(
impl FromBytecode for $ty {
#[inline(always)]
fn read(reader: &mut BytecodeReader<'_>) -> Self {
<$ty>::from_le_bytes(FromBytecode::read(reader))
}
}
)*
};
}
numbers_impl!(u16, u32, i32, i64, f64);
impl FromBytecode for Op {
#[inline(always)]
#[expect(
clippy::panic,
reason = "reads well-formed bytecode; an unknown opcode indicates a codegen bug and should panic loudly"
)]
fn read(reader: &mut BytecodeReader<'_>) -> Self {
reader.inst_start_pc = reader.pc;
let byte: u8 = reader.read();
byte.try_into().unwrap_or_else(|byte| {
std::hint::cold_path();
panic!("unknown opcode: {byte:#04x}")
})
}
}
impl FromBytecode for StringId {
#[inline(always)]
fn read(reader: &mut BytecodeReader<'_>) -> Self {
let raw: u32 = reader.read();
StringId(
string_interner::symbol::SymbolU32::try_from_usize(raw as usize)
.expect("raw is a valid interner symbol index"),
)
}
}
impl FromBytecode for OperandData {
#[inline(always)]
#[expect(
clippy::panic,
reason = "an unknown operand tag in well-formed bytecode indicates a codegen bug and should panic loudly"
)]
fn read(reader: &mut BytecodeReader<'_>) -> Self {
let tag = reader.read();
let ty = OperandType::try_from_primitive(tag).unwrap_or_else(|err| {
std::hint::cold_path();
panic!("unknown operand tag: {:#04x}", err.number)
});
match ty {
OperandType::Const => OperandData::Const(reader.read()),
OperandType::BigInt => OperandData::BigInt(reader.read()),
OperandType::Local => {
let layer = reader.read();
let idx = reader.read();
OperandData::Local { layer, idx }
}
OperandType::BuiltinConst => OperandData::BuiltinConst(reader.read()),
OperandType::Builtins => OperandData::Builtins,
OperandType::ReplBinding => OperandData::ReplBinding(reader.read()),
OperandType::ScopedImportBinding => {
let slot_id = reader.read();
let name = reader.read();
OperandData::ScopedImportBinding { slot_id, name }
}
}
}
}
macro_rules! read_aliases {
{ $($alias:ident -> $ty:ty),*$(,)? } => {
$(
#[inline(always)]
#[must_use]
pub fn $alias(&mut self) -> $ty {
<$ty>::read(self)
}
)*
};
}
impl<'a> BytecodeReader<'a> { impl<'a> BytecodeReader<'a> {
#[must_use]
pub fn new(bytecode: &'a [u8], pc: usize) -> Self { pub fn new(bytecode: &'a [u8], pc: usize) -> Self {
Self { Self {
bytecode, bytecode,
@@ -426,6 +573,7 @@ impl<'a> BytecodeReader<'a> {
} }
#[inline(always)] #[inline(always)]
#[must_use]
pub fn from_after_op(bytecode: &'a [u8], inst_start_pc: usize) -> Self { pub fn from_after_op(bytecode: &'a [u8], inst_start_pc: usize) -> Self {
Self { Self {
bytecode, bytecode,
@@ -435,88 +583,21 @@ impl<'a> BytecodeReader<'a> {
} }
#[inline(always)] #[inline(always)]
#[cfg_attr(debug_assertions, track_caller)] #[must_use]
fn read_array<const N: usize>(&mut self) -> [u8; N] { pub fn read<T: FromBytecode>(&mut self) -> T {
let ret = self.bytecode[self.pc..self.pc + N] T::read(self)
.try_into()
.expect("read_array failed");
self.pc += N;
ret
} }
#[inline(always)] read_aliases! {
pub fn read_op(&mut self) -> Op { read_op -> Op,
self.inst_start_pc = self.pc; read_u8 -> u8,
let byte = self.bytecode[self.pc]; read_u16 -> u16,
if !(0..Op::Illegal as u8).contains(&byte) { read_u32 -> u32,
std::hint::cold_path(); read_i32 -> i32,
panic!("unknown opcode: {byte:#04x}") read_i64 -> i64,
} read_f64 -> f64,
self.pc += 1; read_string_id -> StringId,
unsafe { std::mem::transmute::<u8, Op>(byte) } read_operand_data -> OperandData,
}
#[inline(always)]
pub fn read_u8(&mut self) -> u8 {
let val = self.bytecode[self.pc];
self.pc += 1;
val
}
#[inline(always)]
pub fn read_u16(&mut self) -> u16 {
u16::from_le_bytes(self.read_array())
}
#[inline(always)]
pub fn read_u32(&mut self) -> u32 {
u32::from_le_bytes(self.read_array())
}
#[inline(always)]
pub fn read_i32(&mut self) -> i32 {
i32::from_le_bytes(self.read_array())
}
#[inline(always)]
pub fn read_i64(&mut self) -> i64 {
i64::from_le_bytes(self.read_array())
}
#[inline(always)]
pub fn read_f64(&mut self) -> f64 {
f64::from_le_bytes(self.read_array())
}
#[inline(always)]
pub fn read_string_id(&mut self) -> StringId {
let raw = self.read_u32();
#[allow(clippy::unwrap_used)]
StringId(string_interner::symbol::SymbolU32::try_from_usize(raw as usize).unwrap())
}
#[inline(always)]
pub fn read_operand_data(&mut self) -> OperandData {
let tag = self.read_u8();
let Ok(ty) = OperandType::try_from_primitive(tag)
.map_err(|err| panic!("unknown operand tag: {:#04x}", err.number));
match ty {
OperandType::Const => OperandData::Const(self.read_u32()),
OperandType::BigInt => OperandData::BigInt(self.read_i64()),
OperandType::Local => {
let layer = self.read_u8();
let idx = self.read_u32();
OperandData::Local { layer, idx }
}
OperandType::BuiltinConst => OperandData::BuiltinConst(self.read_string_id()),
OperandType::Builtins => OperandData::Builtins,
OperandType::ReplBinding => OperandData::ReplBinding(self.read_string_id()),
OperandType::ScopedImportBinding => {
let slot_id = self.read_u32();
let name = self.read_string_id();
OperandData::ScopedImportBinding { slot_id, name }
}
}
} }
pub fn pc(&self) -> usize { pub fn pc(&self) -> usize {
+3
View File
@@ -17,3 +17,6 @@ fix-error = { path = "../fix-error" }
fix-lang = { path = "../fix-lang" } fix-lang = { path = "../fix-lang" }
fix-runtime = { path = "../fix-runtime" } fix-runtime = { path = "../fix-runtime" }
tracing = "0.1" tracing = "0.1"
[lints]
workspace = true
+48 -20
View File
@@ -1,5 +1,5 @@
use bumpalo::Bump; use bumpalo::Bump;
use fix_bytecode::{Const, InstructionPtr, Op, Continuation}; use fix_bytecode::{Const, Continuation, InstructionPtr, Op};
use fix_error::{Error, Result, Source}; use fix_error::{Error, Result, Source};
use fix_lang::{StringId, Symbol}; use fix_lang::{StringId, Symbol};
use fix_runtime::{StaticValue, VmCode, VmRuntimeCtx}; use fix_runtime::{StaticValue, VmCode, VmRuntimeCtx};
@@ -114,8 +114,13 @@ impl<'a, R: VmRuntimeCtx> CompilerCtx<'a, R> {
source, source,
); );
let ir = downgrade_ctx.downgrade_toplevel(expr)?; let ir = downgrade_ctx.downgrade_toplevel(expr)?;
// SAFETY: `ir` borrows from `bump`, which is moved into the returned
// `OwnedIr` alongside the reference. The `'static` lifetime is a
// storage token that is re-narrowed to the `OwnedIr`'s own borrow in
// `as_ref`, so no reference ever outlives the arena backing it.
let ir = unsafe { std::mem::transmute::<RawIrRef<'_>, RawIrRef<'static>>(ir) }; let ir = unsafe { std::mem::transmute::<RawIrRef<'_>, RawIrRef<'static>>(ir) };
Ok(OwnedIr { _bump: bump, ir }) // SAFETY: `ir` borrows from `bump`
Ok(unsafe { OwnedIr::new(bump, ir) })
}) })
} }
} }
@@ -148,16 +153,12 @@ impl<'a, R: VmRuntimeCtx> BytecodeContext for CompilerCtx<'a, R> {
fn add_constant(&mut self, val: Const) -> u32 { fn add_constant(&mut self, val: Const) -> u32 {
use Const::*; use Const::*;
let val = match val { let val = match val {
Smi(x) => StaticValue::new_inline(x), Smi(x) => StaticValue::new(x),
Float(x) => StaticValue::new_float(x), Float(x) => StaticValue::new(x),
Bool(x) => StaticValue::new_inline(x), Bool(x) => StaticValue::new(x),
String(x) => StaticValue::new_inline(x), String(x) => StaticValue::new(x),
Path(x) => StaticValue::new_inline(fix_runtime::Path(x)), Path(x) => StaticValue::new(fix_runtime::Path(x)),
PrimOp { PrimOp(id) => StaticValue::new(fix_runtime::PrimOp::from(id)),
id,
arity,
dispatch_ip,
} => StaticValue::new_primop(id, arity, dispatch_ip),
Null => StaticValue::default(), Null => StaticValue::default(),
}; };
self.runtime.add_const(val) self.runtime.add_const(val)
@@ -284,6 +285,10 @@ impl<'ctx: 'ir, 'id, 'ir, R: VmRuntimeCtx> DowngradeContext<'id, 'ir>
self.runtime.resolve_string(id).into() self.runtime.resolve_string(id).into()
} }
#[expect(
clippy::unwrap_in_result,
reason = "exceeding u8::MAX thunk scope layers is a compiler invariant violation, not a user-facing error"
)]
fn lookup( fn lookup(
&mut self, &mut self,
sym: StringId, sym: StringId,
@@ -360,6 +365,10 @@ impl<'ctx: 'ir, 'id, 'ir, R: VmRuntimeCtx> DowngradeContext<'id, 'ir>
self.source.clone() self.source.clone()
} }
#[expect(
clippy::panic_in_result_fn,
reason = "assert_eq guards an internal invariant (bindings and values have equal length); a mismatch is a compiler bug, not a user error"
)]
fn with_let_scope<F, Ret>(&mut self, keys: &[StringId], f: F) -> Result<Ret> fn with_let_scope<F, Ret>(&mut self, keys: &[StringId], f: F) -> Result<Ret>
where where
F: FnOnce( F: FnOnce(
@@ -421,6 +430,10 @@ impl<'ctx: 'ir, 'id, 'ir, R: VmRuntimeCtx> DowngradeContext<'id, 'ir>
ret ret
} }
#[expect(
clippy::panic,
reason = "exceeding u8::MAX thunk scope layers is a compiler invariant violation, not a user-facing error"
)]
fn with_thunk_scope<F, Ret>( fn with_thunk_scope<F, Ret>(
&mut self, &mut self,
f: F, f: F,
@@ -486,7 +499,6 @@ enum Scope<'ctx, 'id, 'ir> {
Repl(&'ctx HashSet<StringId>), Repl(&'ctx HashSet<StringId>),
ScopedImport { ScopedImport {
keys: HashSet<StringId>, keys: HashSet<StringId>,
#[allow(dead_code)]
slot_id: u32, slot_id: u32,
}, },
Let(HashMap<StringId, GhostMaybeThunkRef<'id, 'ir>>), Let(HashMap<StringId, GhostMaybeThunkRef<'id, 'ir>>),
@@ -530,13 +542,29 @@ impl<'a, 'ctx, 'id, 'ir, R: VmRuntimeCtx> ScopeGuard<'a, 'ctx, 'id, 'ir, R> {
} }
} }
struct OwnedIr { mod sealed {
_bump: Bump, use super::*;
ir: RawIrRef<'static>,
}
impl OwnedIr { pub struct OwnedIr {
fn as_ref<'ir>(&'ir self) -> RawIrRef<'ir> { _bump: Bump,
unsafe { std::mem::transmute::<RawIrRef<'static>, RawIrRef<'ir>>(self.ir) } ir: RawIrRef<'static>,
}
impl OwnedIr {
/// # Safety
/// `ir` must borrows from `bump`
pub unsafe fn new(bump: Bump, ir: RawIrRef<'static>) -> Self {
Self { _bump: bump, ir }
}
pub fn as_ref<'ir>(&'ir self) -> RawIrRef<'ir> {
// SAFETY: `self.ir`'s `'static` lifetime is a storage token; the IR is
// backed by `self._bump`, which lives as long as `self`. Narrowing to
// `'ir` (tied to `&self`) hands out a reference that cannot outlive the
// arena.
unsafe { std::mem::transmute::<RawIrRef<'static>, RawIrRef<'ir>>(self.ir) }
}
} }
} }
pub use sealed::OwnedIr;
+3 -5
View File
@@ -3,7 +3,7 @@ use std::marker::PhantomData;
use bumpalo::Bump; use bumpalo::Bump;
use bumpalo::collections::Vec; use bumpalo::collections::Vec;
use fix_lang::{BUILTINS, BuiltinId, StringId}; use fix_lang::{BuiltinId, StringId};
use ghost_cell::{GhostCell, GhostToken}; use ghost_cell::{GhostCell, GhostToken};
use rnix::{TextRange, ast}; use rnix::{TextRange, ast};
use string_interner::DefaultStringInterner; use string_interner::DefaultStringInterner;
@@ -222,7 +222,6 @@ pub enum Ir<'ir, R: RefExt<'ir> + ?Sized + 'ir> {
pub struct ThunkId(pub usize); pub struct ThunkId(pub usize);
/// Represents a key in an attribute path. /// Represents a key in an attribute path.
#[allow(unused)]
#[derive(Debug)] #[derive(Debug)]
pub enum Attr<Ref> { pub enum Attr<Ref> {
/// A dynamic attribute key, which is an expression that must evaluate to a string. /// A dynamic attribute key, which is an expression that must evaluate to a string.
@@ -291,9 +290,8 @@ pub fn new_global_env(
let builtins_sym = StringId(strings.get_or_intern("builtins")); let builtins_sym = StringId(strings.get_or_intern("builtins"));
global_env.insert(builtins_sym, MaybeThunk::Builtins); global_env.insert(builtins_sym, MaybeThunk::Builtins);
for (idx, &(name, _)) in BUILTINS.iter().enumerate() { for id in BuiltinId::ALL {
let id = BuiltinId::try_from(idx as u8).expect("infallible"); let name = StringId(strings.get_or_intern(id.info().global_name));
let name = StringId(strings.get_or_intern(name));
global_env.insert(name, MaybeThunk::Builtin(id)); global_env.insert(name, MaybeThunk::Builtin(id));
} }
+32 -17
View File
@@ -156,6 +156,10 @@ impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> fo
let path = { let path = {
let temp = self.content().require(ctx, span)?; let temp = self.content().require(ctx, span)?;
let text = temp.text(); let text = temp.text();
#[expect(
clippy::string_slice,
reason = "PathSearch text is <...> wrapped in ASCII angle brackets, so byte indices 1 and len-1 are char boundaries"
)]
let id = ctx.intern_string(&text[1..text.len() - 1]); let id = ctx.intern_string(&text[1..text.len() - 1]);
let expr = ctx.new_expr(Ir::Str(id)); let expr = ctx.new_expr(Ir::Str(id));
ctx.maybe_thunk(expr) ctx.maybe_thunk(expr)
@@ -462,16 +466,14 @@ impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> fo
body: GhostRoIrRef<'id, 'ir>, body: GhostRoIrRef<'id, 'ir>,
} }
let (ret, thunks) = ctx.with_thunk_scope(|ctx| { let (ret, thunks) = ctx.with_thunk_scope(|ctx| -> Result<Ret> {
let param; let (param, body) = match raw_param {
let body;
match raw_param {
ast::Param::IdentParam(id) => { ast::Param::IdentParam(id) => {
let param_sym = ctx.intern_string(id.to_string()); let param_sym = ctx.intern_string(id.to_string());
param = None; (
None,
body = ctx.with_param_scope(param_sym, |ctx| body_ast.downgrade(ctx))?; ctx.with_param_scope(param_sym, |ctx| body_ast.downgrade(ctx))?,
)
} }
ast::Param::Pattern(pattern) => { ast::Param::Pattern(pattern) => {
let alias = pattern let alias = pattern
@@ -493,17 +495,18 @@ impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> fo
body_ast.clone().downgrade(ctx) body_ast.clone().downgrade(ctx)
})?; })?;
param = Some(Param { (
required, Some(Param {
optional, required,
ellipsis, optional,
}); ellipsis,
}),
body = inner_body; inner_body,
)
} }
} };
Result::Ok(Ret { param, body }) Ok(Ret { param, body })
}); });
let Ret { param, body } = ret?; let Ret { param, body } = ret?;
@@ -550,6 +553,10 @@ impl<'id: 'ir, 'ir> PendingAttrSet<'ir> {
} }
} }
#[expect(
clippy::indexing_slicing,
reason = "path is non-empty here: path.first() was just unwrapped above, so path[1..] is in bounds"
)]
fn insert( fn insert(
&mut self, &mut self,
path: &[ast::Attr], path: &[ast::Attr],
@@ -651,6 +658,10 @@ impl<'id: 'ir, 'ir> PendingAttrSet<'ir> {
) -> Result<()> { ) -> Result<()> {
if !path.is_empty() { if !path.is_empty() {
let mut nested = PendingAttrSet::new_in(ctx.bump()); let mut nested = PendingAttrSet::new_in(ctx.bump());
#[expect(
clippy::indexing_slicing,
reason = "path is non-empty in this branch, so path[0] and path[1..] are in bounds"
)]
nested.insert_dynamic( nested.insert_dynamic(
path[0].clone(), path[0].clone(),
path[0].syntax().text_range(), path[0].syntax().text_range(),
@@ -665,6 +676,10 @@ impl<'id: 'ir, 'ir> PendingAttrSet<'ir> {
Ok(()) Ok(())
} }
#[expect(
clippy::unreachable,
reason = "value was just reassigned to PendingValue::Set on the line above, so the re-match always hits the Set arm"
)]
fn ensure_pending_set<'a>( fn ensure_pending_set<'a>(
value: &'a mut PendingValue<'ir>, value: &'a mut PendingValue<'ir>,
ctx: &mut impl DowngradeContext<'id, 'ir>, ctx: &mut impl DowngradeContext<'id, 'ir>,
+32 -13
View File
@@ -1,5 +1,5 @@
use fix_bytecode::{Const, InstructionPtr, Op, OperandType, Continuation}; use fix_bytecode::{Const, InstructionPtr, Op, OperandType};
use fix_lang::{BUILTINS, StringId}; use fix_lang::StringId;
use hashbrown::HashMap; use hashbrown::HashMap;
use rnix::TextRange; use rnix::TextRange;
use string_interner::Symbol as _; use string_interner::Symbol as _;
@@ -54,7 +54,6 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
} }
} }
#[must_use]
fn inline_maybe_thunk(&self, val: &MaybeThunk) -> InlineOperand { fn inline_maybe_thunk(&self, val: &MaybeThunk) -> InlineOperand {
use MaybeThunk::*; use MaybeThunk::*;
match *val { match *val {
@@ -75,14 +74,7 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
InlineOperand::Local { layer, local } InlineOperand::Local { layer, local }
} }
Arg { layer } => InlineOperand::Local { layer, local: 0 }, Arg { layer } => InlineOperand::Local { layer, local: 0 },
Builtin(id) => { Builtin(id) => InlineOperand::Const(Const::PrimOp(id)),
let (_, arity) = BUILTINS[id as usize];
InlineOperand::Const(Const::PrimOp {
id,
arity,
dispatch_ip: Continuation::entry_for_builtin(id).ip(),
})
}
BuiltinConst(id) => InlineOperand::BuiltinConst(id), BuiltinConst(id) => InlineOperand::BuiltinConst(id),
Builtins => InlineOperand::Builtins, Builtins => InlineOperand::Builtins,
ReplBinding(id) => InlineOperand::ReplBinding(id), ReplBinding(id) => InlineOperand::ReplBinding(id),
@@ -186,6 +178,10 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
offset offset
} }
#[inline] #[inline]
#[expect(
clippy::indexing_slicing,
reason = "offset addresses a 4-byte i32 placeholder this compiler previously emitted, so it is in bounds"
)]
fn patch_i32(&mut self, offset: usize, val: i32) { fn patch_i32(&mut self, offset: usize, val: i32) {
self.ctx.get_code_mut()[offset..offset + 4].copy_from_slice(&val.to_le_bytes()); self.ctx.get_code_mut()[offset..offset + 4].copy_from_slice(&val.to_le_bytes());
} }
@@ -214,6 +210,10 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
self.scope_stack.last().map_or(0, |s| s.depth) self.scope_stack.last().map_or(0, |s| s.depth)
} }
#[expect(
clippy::panic,
reason = "a ThunkId must resolve in some enclosing scope; failure indicates a compiler bug"
)]
fn resolve_thunk(&self, id: ThunkId) -> (u8, u32) { fn resolve_thunk(&self, id: ThunkId) -> (u8, u32) {
for scope in self.scope_stack.iter().rev() { for scope in self.scope_stack.iter().rev() {
if let Some(&local_idx) = scope.thunk_map.get(&id) { if let Some(&local_idx) = scope.thunk_map.get(&id) {
@@ -520,6 +520,10 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
} }
} }
#[expect(
clippy::unreachable,
reason = "the outer match only reaches this arm for the binary operator kinds enumerated above"
)]
fn emit_binop(&mut self, lhs: RawIrRef<'_>, rhs: RawIrRef<'_>, kind: BinOpKind) { fn emit_binop(&mut self, lhs: RawIrRef<'_>, rhs: RawIrRef<'_>, kind: BinOpKind) {
use BinOpKind::*; use BinOpKind::*;
match kind { match kind {
@@ -712,14 +716,25 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
} }
if let Some(default) = default { if let Some(default) = default {
let before: i32 = self.ctx.get_code().len().try_into().unwrap(); // FIXME: i32???
let before: i32 = self
.ctx
.get_code()
.len()
.try_into()
.expect("emitted code length fits in i32");
for patch in dynamic_patches { for patch in dynamic_patches {
self.patch_jump_target(patch); self.patch_jump_target(patch);
} }
self.emit_op(Op::JumpIfSelectSucceeded); self.emit_op(Op::JumpIfSelectSucceeded);
let placeholder = self.emit_i32_placeholder(); let placeholder = self.emit_i32_placeholder();
self.emit_expr(default); self.emit_expr(default);
let after: i32 = self.ctx.get_code().len().try_into().unwrap(); let after: i32 = self
.ctx
.get_code()
.len()
.try_into()
.expect("emitted code length fits in i32");
// Offset is relative to after the placeholder, so subtract the // Offset is relative to after the placeholder, so subtract the
// size of JumpIfSelectSucceeded (1) + placeholder (4). // size of JumpIfSelectSucceeded (1) + placeholder (4).
self.patch_i32(placeholder, after - before - 5); self.patch_i32(placeholder, after - before - 5);
@@ -730,6 +745,10 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
} }
} }
#[expect(
clippy::panic,
reason = "a hasAttr attrpath always has at least one attr by construction of the AST"
)]
fn emit_has_attr(&mut self, lhs: RawIrRef<'_>, rhs: &[Attr<RawIrRef<'_>>]) { fn emit_has_attr(&mut self, lhs: RawIrRef<'_>, rhs: &[Attr<RawIrRef<'_>>]) {
self.emit_expr(lhs); self.emit_expr(lhs);
+3
View File
@@ -7,3 +7,6 @@ edition = "2024"
miette = { version = "7.6", features = ["fancy"] } miette = { version = "7.6", features = ["fancy"] }
rnix = { workspace = true } rnix = { workspace = true }
thiserror = "2.0" thiserror = "2.0"
[lints]
workspace = true
+1 -1
View File
@@ -4,7 +4,7 @@ use std::sync::Arc;
use miette::{Diagnostic, NamedSource, SourceSpan}; use miette::{Diagnostic, NamedSource, SourceSpan};
use thiserror::Error; use thiserror::Error;
pub type Result<T> = core::result::Result<T, Box<Error>>; pub type Result<T, E = Box<Error>> = core::result::Result<T, E>;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum SourceType { pub enum SourceType {
+3
View File
@@ -8,3 +8,6 @@ ere = { workspace = true }
gc-arena = { workspace = true } gc-arena = { workspace = true }
num_enum = { workspace = true } num_enum = { workspace = true }
string-interner = { workspace = true } string-interner = { workspace = true }
[lints]
workspace = true
+42 -6
View File
@@ -8,7 +8,7 @@ use num_enum::TryFromPrimitive;
macro_rules! define_builtins { macro_rules! define_builtins {
($(($name:literal, $variant:ident, $arity:expr)),* $(,)?) => { ($(($name:literal, $variant:ident, $arity:expr)),* $(,)?) => {
pub const BUILTINS: &[(&str, u8)] = &[ const BUILTINS: &[(&str, u8)] = &[
$(($name, $arity),)* $(($name, $arity),)*
]; ];
@@ -18,6 +18,10 @@ macro_rules! define_builtins {
pub enum BuiltinId { pub enum BuiltinId {
$($variant,)* $($variant,)*
} }
impl BuiltinId {
pub const ALL: [Self; BUILTINS.len()] = [$(Self::$variant,)*];
}
}; };
} }
@@ -127,6 +131,36 @@ define_builtins! {
("__zipAttrsWith", ZipAttrsWith, 2), ("__zipAttrsWith", ZipAttrsWith, 2),
} }
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct BuiltinInfo {
pub name: &'static str,
pub global_name: &'static str,
pub global: bool,
pub arity: u8,
}
impl BuiltinId {
pub const TOTAL: usize = BUILTINS.len();
#[expect(
clippy::indexing_slicing,
reason = "a `BuiltinId` discriminant is always a valid index into the `BUILTINS` table"
)]
#[inline(always)]
pub fn info(self) -> BuiltinInfo {
let (global_name, arity) = BUILTINS[self as usize];
let (name, global) = global_name
.strip_prefix("__")
.map_or((global_name, true), |name| (name, false));
BuiltinInfo {
name,
global_name,
global,
arity,
}
}
}
#[repr(transparent)] #[repr(transparent)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Collect)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Collect)]
#[collect(require_static)] #[collect(require_static)]
@@ -427,6 +461,10 @@ fn fmt_nix_float(f: &mut Formatter<'_>, x: f64) -> FmtResult {
let precision: i32 = 6; let precision: i32 = 6;
let exp = x.abs().log10().floor() as i32; let exp = x.abs().log10().floor() as i32;
#[expect(
clippy::cast_sign_loss,
reason = "this branch runs only when exp < precision, so precision-1-exp and precision-1 are non-negative"
)]
let formatted = if exp >= -4 && exp < precision { let formatted = if exp >= -4 && exp < precision {
let decimal_places = (precision - 1 - exp) as usize; let decimal_places = (precision - 1 - exp) as usize;
format!("{x:.decimal_places$}") format!("{x:.decimal_places$}")
@@ -451,11 +489,9 @@ fn fmt_nix_float(f: &mut Formatter<'_>, x: f64) -> FmtResult {
}; };
if formatted.contains('.') { if formatted.contains('.') {
if let Some(e_pos) = formatted.find('e') { if let Some((head, tail)) = formatted.split_once('e') {
let trimmed = formatted[..e_pos] let trimmed = head.trim_end_matches('0').trim_end_matches('.');
.trim_end_matches('0') write!(f, "{trimmed}e{tail}")
.trim_end_matches('.');
write!(f, "{}{}", trimmed, &formatted[e_pos..])
} else { } else {
let trimmed = formatted.trim_end_matches('0').trim_end_matches('.'); let trimmed = formatted.trim_end_matches('0').trim_end_matches('.');
write!(f, "{trimmed}") write!(f, "{trimmed}")
+15
View File
@@ -0,0 +1,15 @@
[package]
name = "fix-macros"
version = "0.1.0"
edition = "2024"
[lib]
proc-macro = true
[dependencies]
proc-macro2 = "1.0"
quote = "1.0"
syn = { version = "3.0", features = ["full", "visit", "visit-mut"] }
[lints]
workspace = true
File diff suppressed because it is too large Load Diff
+49
View File
@@ -0,0 +1,49 @@
extern crate proc_macro;
mod handler;
#[proc_macro_attribute]
pub fn handler(
attr: proc_macro::TokenStream,
item: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
handler::handler(attr.into(), item.into()).into()
}
// Adapted from `__unelide_lifetimes` in `gc-arena-derive`.
// Licensed under the MIT license.
// See: https://github.com/kyren/gc-arena
#[proc_macro]
pub fn unelide_lifetimes(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
use quote::ToTokens;
use syn::parse::{Parse, ParseStream};
use syn::visit_mut::VisitMut;
struct Input {
lt: syn::Lifetime,
ty: syn::Type,
}
impl Parse for Input {
fn parse(input: ParseStream) -> syn::Result<Self> {
let lt: syn::Lifetime = input.parse()?;
let _: syn::Token!(;) = input.parse()?;
let ty: syn::Type = input.parse()?;
Ok(Self { lt, ty })
}
}
struct UnelideLifetimes(syn::Lifetime);
impl VisitMut for UnelideLifetimes {
fn visit_lifetime_mut(&mut self, i: &mut syn::Lifetime) {
if i.ident == "_" {
*i = self.0.clone();
}
}
}
let mut input = syn::parse_macro_input!(input as Input);
UnelideLifetimes(input.lt).visit_type_mut(&mut input.ty);
input.ty.to_token_stream().into()
}
+4
View File
@@ -13,3 +13,7 @@ string-interner = { workspace = true }
fix-bytecode = { path = "../fix-bytecode" } fix-bytecode = { path = "../fix-bytecode" }
fix-error = { path = "../fix-error" } fix-error = { path = "../fix-error" }
fix-lang = { path = "../fix-lang" } fix-lang = { path = "../fix-lang" }
fix-macros = { path = "../fix-macros" }
[lints]
workspace = true
+76 -30
View File
@@ -1,4 +1,7 @@
#![allow(dead_code)] #![allow(
dead_code,
reason = "boxing layer exposes a full API surface; some helpers are used only on specific targets or not yet wired up"
)]
use std::fmt; use std::fmt;
use std::num::NonZeroU8; use std::num::NonZeroU8;
@@ -79,41 +82,48 @@ int_store!(i16);
int_store!(i32); int_store!(i32);
fn store_ptr<P: Strict + Copy>(value: &mut Value, ptr: P) { fn store_ptr<P: Strict + Copy>(value: &mut Value, ptr: P) {
#[cfg(target_pointer_width = "64")] cfg_select! {
{ target_pointer_width = "64" => {
assert!( assert!(
ptr.addr() <= 0x0000_FFFF_FFFF_FFFF, ptr.addr() <= 0x0000_FFFF_FFFF_FFFF,
"Pointer too large to store in NaN box" "Pointer too large to store in NaN box"
); );
let val = (unsafe { value.whole_mut() } as *mut [u8; 8]).cast::<P>(); // SAFETY: `Value` is `#[repr(C, align(8))]` and exactly 8 bytes, so it is
// sound to reinterpret its storage as a `[u8; 8]` that we then treat as `P`
// (a pointer-sized value).
let val = (unsafe { value.whole_mut() } as *mut [u8; 8]).cast::<P>();
let ptr = Strict::map_addr(ptr, |addr| { let ptr = Strict::map_addr(ptr, |addr| {
addr | (usize::from(value.header().into_raw()) << 48) addr | (usize::from(value.header().into_raw()) << 48)
}); });
unsafe { val.write(ptr) }; // SAFETY: `val` points to the `Value`'s 8-byte storage, which is valid and
} // suitably aligned to hold `P`.
unsafe { val.write(ptr) };
#[cfg(target_pointer_width = "32")] }
{ _ => {
let _ = (value, ptr); compile_error!("unsupported pointer width");
unimplemented!("32-bit pointer storage not supported"); }
} }
} }
fn load_ptr<P: Strict>(value: &Value) -> P { fn load_ptr<P: Strict>(value: &Value) -> P {
#[cfg(target_pointer_width = "64")] cfg_select! {
{ target_pointer_width = "64" => {
let val = (unsafe { value.whole() } as *const [u8; 8]).cast::<P>(); // SAFETY: `Value` is `#[repr(C, align(8))]` and exactly 8 bytes, so it
let ptr = unsafe { val.read() }; // is sound to reinterpret its storage as a `[u8; 8]` cast to `P`; the
Strict::map_addr(ptr, |addr| addr & 0x0000_FFFF_FFFF_FFFF) // pointer was originally written through this same `P` layout by
} // `store_ptr`.
let val = (unsafe { value.whole() } as *const [u8; 8]).cast::<P>();
#[cfg(target_pointer_width = "32")] // SAFETY: `val` points to the `Value`'s 8-byte storage, which is valid
{ // and suitably aligned to hold `P`.
let _ = value; let ptr = unsafe { val.read() };
unimplemented!("32-bit pointer storage not supported"); Strict::map_addr(ptr, |addr| addr & 0x0000_FFFF_FFFF_FFFF)
}
_ => {
compile_error!("unsupported pointer width");
}
} }
} }
@@ -179,6 +189,10 @@ impl RawTag {
#[inline] #[inline]
#[must_use] #[must_use]
pub(crate) fn new(neg: bool, val: NonZeroU8) -> RawTag { pub(crate) fn new(neg: bool, val: NonZeroU8) -> RawTag {
// SAFETY: masking a `NonZeroU8` with `0x07` yields a value in `0..8`;
// callers only construct tags from valid tag discriminants in `1..8`,
// so the low three bits are in the `1..8` range required by
// `new_unchecked`.
unsafe { Self::new_unchecked(neg, val.get() & 0x07) } unsafe { Self::new_unchecked(neg, val.get() & 0x07) }
} }
@@ -229,6 +243,9 @@ impl RawTag {
(true, 6) => TagVal::_N6, (true, 6) => TagVal::_N6,
(true, 7) => TagVal::_N7, (true, 7) => TagVal::_N7,
// SAFETY: the caller guarantees `val` is in `1..8`; every
// `(neg, val)` combination in that range is matched above, so this
// arm cannot be reached.
_ => unsafe { core::hint::unreachable_unchecked() }, _ => unsafe { core::hint::unreachable_unchecked() },
}) })
} }
@@ -302,6 +319,10 @@ impl Header {
#[inline] #[inline]
const fn tag(self) -> RawTag { const fn tag(self) -> RawTag {
// SAFETY: a `Header` is only ever constructed by `Header::new` from a
// `RawTag` whose value is in `1..8`, stored in the low three bits;
// `get_tag` recovers exactly those bits, so the argument passed to
// `new_unchecked` is in the required `1..8` range.
unsafe { RawTag::new_unchecked(self.get_sign(), self.get_tag()) } unsafe { RawTag::new_unchecked(self.get_sign(), self.get_tag()) }
} }
@@ -323,7 +344,7 @@ impl Header {
#[derive(Copy, Clone, Debug, PartialEq)] #[derive(Copy, Clone, Debug, PartialEq)]
#[repr(C, align(8))] #[repr(C, align(8))]
pub struct Value { pub(crate) struct Value {
#[cfg(target_endian = "big")] #[cfg(target_endian = "big")]
header: Header, header: Header,
data: [u8; 6], data: [u8; 6],
@@ -387,6 +408,9 @@ impl Value {
#[must_use] #[must_use]
unsafe fn whole(&self) -> &[u8; 8] { unsafe fn whole(&self) -> &[u8; 8] {
let ptr = (self as *const Value).cast::<[u8; 8]>(); let ptr = (self as *const Value).cast::<[u8; 8]>();
// SAFETY: `Value` is `#[repr(C, align(8))]` and exactly 8 bytes with no
// padding, so it shares its layout with `[u8; 8]`; `ptr` is derived
// from a valid `&Value`, so the reference is valid for reads.
unsafe { &*ptr } unsafe { &*ptr }
} }
@@ -394,6 +418,10 @@ impl Value {
#[must_use] #[must_use]
unsafe fn whole_mut(&mut self) -> &mut [u8; 8] { unsafe fn whole_mut(&mut self) -> &mut [u8; 8] {
let ptr = (self as *mut Value).cast::<[u8; 8]>(); let ptr = (self as *mut Value).cast::<[u8; 8]>();
// SAFETY: `Value` is `#[repr(C, align(8))]` and exactly 8 bytes with no
// padding, so it shares its layout with `[u8; 8]`; `ptr` is derived
// from a unique `&mut Value`, so the reference is valid for reads and
// writes.
unsafe { &mut *ptr } unsafe { &mut *ptr }
} }
} }
@@ -435,6 +463,9 @@ impl RawBox {
#[must_use] #[must_use]
pub(crate) const fn tag(&self) -> Option<RawTag> { pub(crate) const fn tag(&self) -> Option<RawTag> {
if self.is_value() { if self.is_value() {
// SAFETY: `is_value()` returned true, so the union holds a `Value`
// (a tagged-NaN bit pattern) rather than a float, making the read
// of the `value` field sound.
Some(unsafe { self.value.tag() }) Some(unsafe { self.value.tag() })
} else { } else {
None None
@@ -444,12 +475,18 @@ impl RawBox {
#[inline] #[inline]
#[must_use] #[must_use]
pub(crate) fn is_float(&self) -> bool { pub(crate) fn is_float(&self) -> bool {
// SAFETY: every 8-byte pattern is simultaneously a valid `f64` and a
// valid `u64`, so reading the `float` and `bits` union fields is
// always sound.
(unsafe { !self.float.is_nan() } || unsafe { self.bits & SIGN_MASK == QUIET_NAN }) (unsafe { !self.float.is_nan() } || unsafe { self.bits & SIGN_MASK == QUIET_NAN })
} }
#[inline] #[inline]
#[must_use] #[must_use]
pub(crate) const fn is_value(&self) -> bool { pub(crate) const fn is_value(&self) -> bool {
// SAFETY: every 8-byte pattern is simultaneously a valid `f64` and a
// valid `u64`, so reading the `float` and `bits` union fields is
// always sound.
(unsafe { self.float.is_nan() } && unsafe { self.bits & SIGN_MASK != QUIET_NAN }) (unsafe { self.float.is_nan() } && unsafe { self.bits & SIGN_MASK != QUIET_NAN })
} }
@@ -457,6 +494,8 @@ impl RawBox {
#[must_use] #[must_use]
pub(crate) fn float(&self) -> Option<&f64> { pub(crate) fn float(&self) -> Option<&f64> {
if self.is_float() { if self.is_float() {
// SAFETY: reading the `float` field is sound because any 8-byte
// pattern is a valid `f64`.
Some(unsafe { &self.float }) Some(unsafe { &self.float })
} else { } else {
None None
@@ -467,6 +506,9 @@ impl RawBox {
#[must_use] #[must_use]
pub(crate) fn value(&self) -> Option<&Value> { pub(crate) fn value(&self) -> Option<&Value> {
if self.is_value() { if self.is_value() {
// SAFETY: `is_value()` returned true, so the union holds a `Value`;
// `Value` has no invalid bit patterns, so reading the `value` field
// is sound.
Some(unsafe { &self.value }) Some(unsafe { &self.value })
} else { } else {
None None
@@ -475,12 +517,16 @@ impl RawBox {
#[inline] #[inline]
pub(crate) fn into_float_unchecked(self) -> f64 { pub(crate) fn into_float_unchecked(self) -> f64 {
// SAFETY: reading the `float` field is sound because any 8-byte pattern
// is a valid `f64`.
unsafe { self.float } unsafe { self.float }
} }
#[inline] #[inline]
#[must_use] #[must_use]
pub(crate) fn to_bits(self) -> u64 { pub(crate) fn to_bits(self) -> u64 {
// SAFETY: reading the `bits` field is sound because any 8-byte pattern
// is a valid `u64`.
unsafe { self.bits } unsafe { self.bits }
} }
} }
+33 -70
View File
@@ -1,9 +1,9 @@
use fix_lang::StringId; use fix_lang::StringId;
use gc_arena::{Gc, Mutation}; use gc_arena::Mutation;
use crate::{ use crate::{
AttrSet, Break, BytecodeReader, Closure, List, Machine, NixNum, NixString, NixType, Null, AttrSet, BytecodeReader, Closure, List, Machine, NixNum, NixString, NixType, Null, PrimOp,
PrimOp, PrimOpApp, Step, StrictValue, PrimOpApp, Step, StrictValue, ValueVariant,
}; };
pub trait Forced<'gc>: Sized { pub trait Forced<'gc>: Sized {
@@ -47,10 +47,10 @@ impl<'gc> Forced<'gc> for StrictValue<'gc> {
} }
} }
macro_rules! impl_forced_inline { macro_rules! impl_forced {
($($ty:ty => $nix_ty:expr),* $(,)?) => { ($($ty:ty),* $(,)?) => {
$( $(
impl<'gc> Forced<'gc> for $ty { impl<'gc> Forced<'gc> for <$ty as ValueVariant<'gc>>::Ty {
const WIDTH: usize = 1; const WIDTH: usize = 1;
#[inline(always)] #[inline(always)]
@@ -63,17 +63,17 @@ macro_rules! impl_forced_inline {
) -> Step { ) -> Step {
m.force_slot_to_pc(base_depth, reader, mc, resume_pc)?; m.force_slot_to_pc(base_depth, reader, mc, resume_pc)?;
let v = m.peek_forced(base_depth); let v = m.peek_forced(base_depth);
if v.as_inline::<$ty>().is_none() { if !v.is::<$ty>() {
let _: Step = m.finish_type_err($nix_ty, v.ty()); m.finish_type_err(<$ty as ValueVariant>::TYPE, v.ty())
return Step::Break(Break::Done); } else {
Step::Continue(())
} }
Step::Continue(())
} }
#[inline(always)] #[inline(always)]
fn pop_converted<M: Machine<'gc>>(m: &mut M) -> Self { fn pop_converted<M: Machine<'gc>>(m: &mut M) -> Self {
m.pop_forced() m.pop_forced()
.as_inline::<$ty>() .downcast::<$ty>()
.expect("type checked in force_and_check") .expect("type checked in force_and_check")
} }
} }
@@ -81,55 +81,18 @@ macro_rules! impl_forced_inline {
}; };
} }
macro_rules! impl_forced_gc { impl_forced! {
($($ty:ty => $nix_ty:expr),* $(,)?) => { i32,
$( bool,
impl<'gc> Forced<'gc> for Gc<'gc, $ty> { Null,
const WIDTH: usize = 1; StringId,
PrimOp,
#[inline(always)] i64,
fn force_and_check<M: Machine<'gc>>( NixString,
m: &mut M, AttrSet<'gc>,
reader: &mut BytecodeReader<'_>, List<'gc>,
mc: &Mutation<'gc>, Closure<'gc>,
base_depth: usize, PrimOpApp<'gc>,
resume_pc: usize,
) -> Step {
m.force_slot_to_pc(base_depth, reader, mc, resume_pc)?;
let v = m.peek_forced(base_depth);
if v.as_gc::<$ty>().is_none() {
let _: Step = m.finish_type_err($nix_ty, v.ty());
return Step::Break(Break::Done);
}
Step::Continue(())
}
#[inline(always)]
fn pop_converted<M: Machine<'gc>>(m: &mut M) -> Self {
m.pop_forced()
.as_gc::<$ty>()
.expect("type checked in force_and_check")
}
}
)*
};
}
impl_forced_inline! {
i32 => NixType::Int,
bool => NixType::Bool,
Null => NixType::Null,
StringId => NixType::String,
PrimOp => NixType::PrimOp,
}
impl_forced_gc! {
i64 => NixType::Int,
NixString => NixType::String,
AttrSet<'gc> => NixType::AttrSet,
List<'gc> => NixType::List,
Closure<'gc> => NixType::Closure,
PrimOpApp<'gc> => NixType::PrimOpApp,
} }
impl<'gc> Forced<'gc> for NixNum { impl<'gc> Forced<'gc> for NixNum {
@@ -145,17 +108,17 @@ impl<'gc> Forced<'gc> for NixNum {
) -> Step { ) -> Step {
m.force_slot_to_pc(base_depth, reader, mc, resume_pc)?; m.force_slot_to_pc(base_depth, reader, mc, resume_pc)?;
let v = m.peek_forced(base_depth); let v = m.peek_forced(base_depth);
if v.as_num().is_none() { if v.downcast_num().is_none() {
let _: Step = m.finish_type_err(NixType::Int, v.ty()); m.finish_type_err(NixType::Int, v.ty())
return Step::Break(Break::Done); } else {
Step::Continue(())
} }
Step::Continue(())
} }
#[inline(always)] #[inline(always)]
fn pop_converted<M: Machine<'gc>>(m: &mut M) -> Self { fn pop_converted<M: Machine<'gc>>(m: &mut M) -> Self {
m.pop_forced() m.pop_forced()
.as_num() .downcast_num()
.expect("type checked in force_and_check") .expect("type checked in force_and_check")
} }
} }
@@ -173,17 +136,17 @@ impl<'gc> Forced<'gc> for f64 {
) -> Step { ) -> Step {
m.force_slot_to_pc(base_depth, reader, mc, resume_pc)?; m.force_slot_to_pc(base_depth, reader, mc, resume_pc)?;
let v = m.peek_forced(base_depth); let v = m.peek_forced(base_depth);
if v.as_float().is_none() { if !v.is::<f64>() {
let _: Step = m.finish_type_err(NixType::Float, v.ty()); m.finish_type_err(NixType::Float, v.ty())
return Step::Break(Break::Done); } else {
Step::Continue(())
} }
Step::Continue(())
} }
#[inline(always)] #[inline(always)]
fn pop_converted<M: Machine<'gc>>(m: &mut M) -> Self { fn pop_converted<M: Machine<'gc>>(m: &mut M) -> Self {
m.pop_forced() m.pop_forced()
.as_float() .downcast::<f64>()
.expect("type checked in force_and_check") .expect("type checked in force_and_check")
} }
} }
+21 -23
View File
@@ -1,6 +1,6 @@
use fix_bytecode::InstructionPtr; use fix_bytecode::InstructionPtr;
use fix_error::Source; use fix_error::Source;
use fix_lang::{self, BUILTINS, StringId}; use fix_lang::{self, StringId};
use hashbrown::HashSet; use hashbrown::HashSet;
use crate::{ use crate::{
@@ -44,10 +44,10 @@ pub trait VmRuntimeCtxExt: VmRuntimeCtx {
impl<T: VmRuntimeCtx> VmRuntimeCtxExt for T { impl<T: VmRuntimeCtx> VmRuntimeCtxExt for T {
fn get_string<'a, 'gc: 'a>(&'a self, val: StrictValue<'gc>) -> Option<&'a str> { fn get_string<'a, 'gc: 'a>(&'a self, val: StrictValue<'gc>) -> Option<&'a str> {
if let Some(sid) = val.as_inline::<StringId>() { if let Some(sid) = val.downcast::<StringId>() {
Some(self.resolve_string(sid)) Some(self.resolve_string(sid))
} else { } else {
val.as_gc::<NixString>().map(|ns| ns.as_ref().as_str()) val.downcast::<NixString>().map(|ns| ns.as_ref().as_str())
} }
} }
@@ -56,7 +56,7 @@ impl<T: VmRuntimeCtx> VmRuntimeCtxExt for T {
/// would coerce a path to a string (string interpolation, file IO /// would coerce a path to a string (string interpolation, file IO
/// builtins, etc.). /// builtins, etc.).
fn get_string_or_path<'a, 'gc: 'a>(&'a self, val: StrictValue<'gc>) -> Option<&'a str> { fn get_string_or_path<'a, 'gc: 'a>(&'a self, val: StrictValue<'gc>) -> Option<&'a str> {
if let Some(p) = val.as_inline::<Path>() { if let Some(p) = val.downcast::<Path>() {
Some(self.resolve_string(p.0)) Some(self.resolve_string(p.0))
} else { } else {
self.get_string(val) self.get_string(val)
@@ -67,9 +67,9 @@ impl<T: VmRuntimeCtx> VmRuntimeCtxExt for T {
&'a mut self, &'a mut self,
val: StrictValue<'gc>, val: StrictValue<'gc>,
) -> std::result::Result<StringId, NixType> { ) -> std::result::Result<StringId, NixType> {
if let Some(sid) = val.as_inline::<StringId>() { if let Some(sid) = val.downcast::<StringId>() {
Ok(sid) Ok(sid)
} else if let Some(s) = val.as_gc::<NixString>().map(|ns| ns.as_ref().as_str()) { } else if let Some(s) = val.downcast::<NixString>().map(|ns| ns.as_ref().as_str()) {
Ok(self.intern_string(s)) Ok(self.intern_string(s))
} else { } else {
Err(val.ty()) Err(val.ty())
@@ -77,7 +77,7 @@ impl<T: VmRuntimeCtx> VmRuntimeCtxExt for T {
} }
fn get_string_context<'gc>(&self, val: StrictValue<'gc>) -> &'gc StringContext { fn get_string_context<'gc>(&self, val: StrictValue<'gc>) -> &'gc StringContext {
if let Some(ns) = val.as_gc::<NixString>() { if let Some(ns) = val.downcast::<NixString>() {
ns.as_ref().context() ns.as_ref().context()
} else { } else {
StringContext::empty() StringContext::empty()
@@ -96,24 +96,24 @@ pub(crate) trait ConvertValueWithSeen: VmRuntimeCtx {
impl<T: VmRuntimeCtx> ConvertValueWithSeen for T { impl<T: VmRuntimeCtx> ConvertValueWithSeen for T {
fn convert_value_with_seen(&self, val: Value, seen: &mut HashSet<u64>) -> fix_lang::Value { fn convert_value_with_seen(&self, val: Value, seen: &mut HashSet<u64>) -> fix_lang::Value {
use fix_lang::Value; use fix_lang::Value;
if let Some(i) = val.as_inline::<i32>() { if let Some(i) = val.downcast::<i32>() {
Value::Int(i as i64) Value::Int(i as i64)
} else if let Some(gc_i) = val.as_gc::<i64>() { } else if let Some(gc_i) = val.downcast::<i64>() {
Value::Int(*gc_i) Value::Int(*gc_i)
} else if let Some(f) = val.as_float() { } else if let Some(f) = val.downcast::<f64>() {
Value::Float(f) Value::Float(f)
} else if let Some(b) = val.as_inline::<bool>() { } else if let Some(b) = val.downcast::<bool>() {
Value::Bool(b) Value::Bool(b)
} else if val.is::<Null>() { } else if val.is::<Null>() {
Value::Null Value::Null
} else if let Some(sid) = val.as_inline::<StringId>() { } else if let Some(sid) = val.downcast::<StringId>() {
let s = self.resolve_string(sid).to_owned(); let s = self.resolve_string(sid).to_owned();
Value::String(s) Value::String(s)
} else if let Some(ns) = val.as_gc::<NixString>() { } else if let Some(ns) = val.downcast::<NixString>() {
Value::String(ns.as_str().to_owned()) Value::String(ns.as_str().to_owned())
} else if let Some(p) = val.as_inline::<Path>() { } else if let Some(p) = val.downcast::<Path>() {
Value::Path(self.resolve_string(p.0).to_owned()) Value::Path(self.resolve_string(p.0).to_owned())
} else if let Some(attrs) = val.as_gc::<AttrSet>() { } else if let Some(attrs) = val.downcast::<AttrSet>() {
let bits = val.to_bits(); let bits = val.to_bits();
if attrs.entries.is_empty() { if attrs.entries.is_empty() {
return Value::AttrSet(Default::default()); return Value::AttrSet(Default::default());
@@ -128,7 +128,7 @@ impl<T: VmRuntimeCtx> ConvertValueWithSeen for T {
map.insert(fix_lang::Symbol::from(key), converted); map.insert(fix_lang::Symbol::from(key), converted);
} }
Value::AttrSet(fix_lang::AttrSet::new(map)) Value::AttrSet(fix_lang::AttrSet::new(map))
} else if let Some(list) = val.as_gc::<List>() { } else if let Some(list) = val.downcast::<List>() {
let bits = val.to_bits(); let bits = val.to_bits();
if list.inner.borrow().is_empty() { if list.inner.borrow().is_empty() {
return Value::List(Default::default()); return Value::List(Default::default());
@@ -146,18 +146,16 @@ impl<T: VmRuntimeCtx> ConvertValueWithSeen for T {
Value::List(fix_lang::List::new(items)) Value::List(fix_lang::List::new(items))
} else if val.is::<Closure>() { } else if val.is::<Closure>() {
Value::Func Value::Func
} else if let Some(thunk) = val.as_gc::<Thunk>() { } else if let Some(thunk) = val.downcast::<Thunk>() {
if let ThunkState::Evaluated(v) = *thunk.borrow() { if let ThunkState::Evaluated(v) = *thunk.borrow() {
self.convert_value_with_seen(v.relax(), seen) self.convert_value_with_seen(v.relax(), seen)
} else { } else {
Value::Thunk Value::Thunk
} }
} else if let Some(primop) = val.as_inline::<PrimOp>() { } else if let Some(primop) = val.downcast::<PrimOp>() {
let name = BUILTINS[primop.id as usize].0; Value::PrimOp(primop.id.info().name)
Value::PrimOp(name.strip_prefix("__").unwrap_or(name)) } else if let Some(app) = val.downcast::<PrimOpApp>() {
} else if let Some(app) = val.as_gc::<PrimOpApp>() { Value::PrimOpApp(app.primop.id.info().name)
let name = BUILTINS[app.primop.id as usize].0;
Value::PrimOpApp(name.strip_prefix("__").unwrap_or(name))
} else { } else {
Value::Null Value::Null
} }
+4
View File
@@ -2,8 +2,10 @@ mod boxing;
mod forced; mod forced;
mod host; mod host;
mod machine; mod machine;
mod macro_support;
mod path_util; mod path_util;
mod resolve; mod resolve;
mod slot;
mod state; mod state;
mod string_context; mod string_context;
mod value; mod value;
@@ -12,8 +14,10 @@ pub use fix_bytecode::{BytecodeReader, OperandData};
pub use forced::*; pub use forced::*;
pub use host::*; pub use host::*;
pub use machine::*; pub use machine::*;
pub use macro_support::*;
pub use path_util::*; pub use path_util::*;
pub use resolve::*; pub use resolve::*;
pub use slot::*;
pub use state::*; pub use state::*;
pub use string_context::*; pub use string_context::*;
pub use value::*; pub use value::*;
+13 -4
View File
@@ -3,11 +3,11 @@ use std::path::{Path, PathBuf};
use fix_error::Error; use fix_error::Error;
use fix_lang::{self, StringId}; use fix_lang::{self, StringId};
use gc_arena::Mutation; use gc_arena::{Gc, Mutation};
use crate::{ use crate::{
Break, BytecodeReader, CallFrame, ForceMode, Forced, GcEnv, NixType, PendingLoad, Step, AttrSet, Break, BytecodeReader, CallFrame, ForceMode, Forced, GcEnv, NixType, PendingLoad,
StrictValue, Value, VmError, Step, StrictValue, Value, VmError,
}; };
/// Abstract VM-side operations consumed by instruction handlers and primops. /// Abstract VM-side operations consumed by instruction handlers and primops.
@@ -65,12 +65,17 @@ pub trait Machine<'gc> {
) -> Step; ) -> Step;
#[inline(always)] #[inline(always)]
#[expect(
clippy::unreachable,
reason = "a primop only returns via `return_from_primop` while its call frame is still on the stack, so `pop_call_frame` is always `Some`"
)]
fn return_from_primop(&mut self, val: Value<'gc>, reader: &mut BytecodeReader<'_>) -> Step { fn return_from_primop(&mut self, val: Value<'gc>, reader: &mut BytecodeReader<'_>) -> Step {
self.push(val); self.push(val);
let Some(CallFrame { let Some(CallFrame {
pc: ret_pc, pc: ret_pc,
thunk: _, thunk: _,
env, env,
depth: None,
}) = self.pop_call_frame() }) = self.pop_call_frame()
else { else {
unreachable!() unreachable!()
@@ -91,6 +96,10 @@ pub trait Machine<'gc> {
fn set_env(&mut self, env: GcEnv<'gc>); fn set_env(&mut self, env: GcEnv<'gc>);
#[inline(always)] #[inline(always)]
#[expect(
clippy::indexing_slicing,
reason = "codegen guarantees the local index is within the resolved frame's `locals`"
)]
fn local(&self, layer: u8, idx: u32) -> Value<'gc> { fn local(&self, layer: u8, idx: u32) -> Value<'gc> {
let mut cur = self.env(); let mut cur = self.env();
for _ in 0..layer { for _ in 0..layer {
@@ -109,7 +118,7 @@ pub trait Machine<'gc> {
self.finish_err(err.into_error()) self.finish_err(err.into_error())
} }
fn builtins(&self) -> Value<'gc>; fn builtins(&self) -> Gc<'gc, AttrSet<'gc>>;
fn functor_sym(&self) -> StringId; fn functor_sym(&self) -> StringId;
fn empty_list(&self) -> Value<'gc>; fn empty_list(&self) -> Value<'gc>;
fn empty_attrs(&self) -> Value<'gc>; fn empty_attrs(&self) -> Value<'gc>;
+165
View File
@@ -0,0 +1,165 @@
use std::ops::ControlFlow;
use gc_arena::Mutation;
use crate::{
Break, BytecodeReader, Forced, Machine, MachineExt, Slot, SlotContent, Step, StrictValue,
Value, ValueVariant,
};
pub trait UnwrapSlot {
type Unwrapped;
}
impl<T> UnwrapSlot for Slot<T> {
type Unwrapped = T;
}
/// Force-target protocol behind the `#[primop]` `force` form: `Value` only
/// needs WHNF, `StrictValue` *is* WHNF, and content types are checked through
/// [`Forced`] on their stored representation.
pub trait ForceTarget<'gc>: SlotContent<'gc> {
/// Type-check the stack slot refined by the previous `force(slot)`; the
/// slot is already WHNF when this runs.
fn refine_check<M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
depth: usize,
) -> Step;
/// Pop and convert the previous suspension's result into the annotated
/// binding's stored representation.
fn conv_force<M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> ControlFlow<Break, <Self as SlotContent<'gc>>::Ty>;
}
impl<'gc> ForceTarget<'gc> for Value<'gc> {
#[inline(always)]
fn refine_check<M: Machine<'gc>>(
_m: &mut M,
_reader: &mut BytecodeReader<'_>,
_mc: &Mutation<'gc>,
_depth: usize,
) -> Step {
Step::Continue(())
}
#[inline(always)]
fn conv_force<M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> ControlFlow<Break, Value<'gc>> {
match m.force_and_retry::<StrictValue<'gc>>(reader, mc) {
ControlFlow::Continue(v) => ControlFlow::Continue(v.relax()),
ControlFlow::Break(b) => ControlFlow::Break(b),
}
}
}
impl<'gc> ForceTarget<'gc> for StrictValue<'gc> {
#[inline(always)]
fn refine_check<M: Machine<'gc>>(
_m: &mut M,
_reader: &mut BytecodeReader<'_>,
_mc: &Mutation<'gc>,
_depth: usize,
) -> Step {
Step::Continue(())
}
#[inline(always)]
fn conv_force<M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> ControlFlow<Break, StrictValue<'gc>> {
m.force_and_retry::<StrictValue<'gc>>(reader, mc)
}
}
impl<'gc, T> ForceTarget<'gc> for T
where
T: ValueVariant<'gc>,
<T as ValueVariant<'gc>>::Ty: Forced<'gc>,
{
#[inline(always)]
fn refine_check<M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
depth: usize,
) -> Step {
<<T as ValueVariant<'gc>>::Ty as Forced<'gc>>::force_and_check(
m,
reader,
mc,
depth,
reader.inst_start_pc(),
)
}
#[inline(always)]
fn conv_force<M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> ControlFlow<Break, <T as SlotContent<'gc>>::Ty> {
m.force_and_retry::<<T as ValueVariant<'gc>>::Ty>(reader, mc)
}
}
/// Callee protocol behind `call(&slot, ..)`: an unforced `Slot<Value>` is
/// WHNF'd before the call; refined slots already hold strict values.
pub trait CalleeReady<'gc> {
fn callee_ready<M: Machine<'gc>>(
&self,
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> ControlFlow<Break>;
}
impl<'gc> CalleeReady<'gc> for Slot<Value<'gc>> {
#[inline(always)]
fn callee_ready<M: Machine<'gc>>(
&self,
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> ControlFlow<Break> {
self.force::<StrictValue<'gc>, M>(m, reader, mc)?;
ControlFlow::Continue(())
}
}
impl<'gc> CalleeReady<'gc> for Slot<StrictValue<'gc>> {
#[inline(always)]
fn callee_ready<M: Machine<'gc>>(
&self,
_m: &mut M,
_reader: &mut BytecodeReader<'_>,
_mc: &Mutation<'gc>,
) -> ControlFlow<Break> {
ControlFlow::Continue(())
}
}
impl<'gc, T> CalleeReady<'gc> for Slot<T>
where
T: ValueVariant<'gc>,
{
#[inline(always)]
fn callee_ready<M: Machine<'gc>>(
&self,
_m: &mut M,
_reader: &mut BytecodeReader<'_>,
_mc: &Mutation<'gc>,
) -> ControlFlow<Break> {
ControlFlow::Continue(())
}
}
+4 -7
View File
@@ -18,17 +18,14 @@ pub fn resolve_operand<'gc, M: Machine<'gc>>(
use OperandData::*; use OperandData::*;
match *op { match *op {
Const(id) => ctx.get_const(id).into(), Const(id) => ctx.get_const(id).into(),
BigInt(val) => Value::new_gc(Gc::new(mc, val)), BigInt(val) => Value::new(Gc::new(mc, val)),
Local { layer, idx } => m.local(layer, idx), Local { layer, idx } => m.local(layer, idx),
#[allow(clippy::unwrap_used)] BuiltinConst(id) => m.builtins().lookup(id).expect("builtin const must exist"),
BuiltinConst(id) => m.builtins().as_gc::<AttrSet>().unwrap().lookup(id).unwrap(), Builtins => m.builtins().into(),
Builtins => m.builtins(),
ReplBinding(_id) => todo!(), ReplBinding(_id) => todo!(),
ScopedImportBinding { slot_id, name } => { ScopedImportBinding { slot_id, name } => {
let scope = m.scope_slot(slot_id); let scope = m.scope_slot(slot_id);
#[allow(clippy::unwrap_used)] let attrs = scope.downcast::<AttrSet>().expect("scope must be attrset");
let attrs = scope.as_gc::<AttrSet>().expect("scope must be attrset");
#[allow(clippy::unwrap_used)]
attrs.lookup(name).expect("scoped binding not found") attrs.lookup(name).expect("scoped binding not found")
} }
} }
+86
View File
@@ -0,0 +1,86 @@
use std::marker::PhantomData;
use std::ops::ControlFlow;
use fix_bytecode::BytecodeReader;
use gc_arena::Mutation;
use crate::{Break, Forced, Machine, NixType, Step, StrictValue, Value, ValueVariant};
pub struct TypeError {
pub expected: NixType,
pub got: NixType,
}
pub trait SlotContent<'gc> {
type Ty: Into<Value<'gc>> + TryFrom<Value<'gc>> + 'gc;
}
impl<'gc> SlotContent<'gc> for Value<'gc> {
type Ty = Value<'gc>;
}
impl<'gc> SlotContent<'gc> for StrictValue<'gc> {
type Ty = StrictValue<'gc>;
}
impl<'gc, T> SlotContent<'gc> for T
where
T: ValueVariant<'gc>,
{
type Ty = T::Ty;
}
pub struct Slot<T> {
depth: u8,
_marker: PhantomData<T>,
}
impl<T> Slot<T> {
#[inline(always)]
pub const fn new(depth: u8) -> Self {
Self {
depth,
_marker: PhantomData,
}
}
}
impl<'gc, T> Slot<T>
where
T: SlotContent<'gc>,
<T::Ty as TryFrom<Value<'gc>>>::Error: std::fmt::Debug,
{
#[inline(always)]
pub fn read<M: Machine<'gc>>(&self, m: &M) -> T::Ty {
T::Ty::try_from(m.peek(self.depth as usize)).expect("slot held a value of the wrong type")
}
#[inline(always)]
pub fn write<M: Machine<'gc>>(&self, m: &mut M, val: T::Ty) {
m.replace(self.depth as usize, T::Ty::into(val));
}
}
impl<'gc> Slot<Value<'gc>> {
#[inline(always)]
pub fn force<T: Forced<'gc>, M: Machine<'gc>>(
&self,
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> ControlFlow<Break, Slot<T>> {
T::force_and_check(m, reader, mc, self.depth as usize, reader.inst_start_pc())?;
ControlFlow::Continue(Slot::new(self.depth))
}
#[inline(always)]
pub fn force_to_pc<M: Machine<'gc>>(
&self,
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
pc: usize,
) -> Step {
m.force_slot_to_pc(self.depth as usize, reader, mc, pc)
}
}
+2 -3
View File
@@ -8,7 +8,6 @@ use hashbrown::HashSet;
use crate::{GcEnv, Thunk}; use crate::{GcEnv, Thunk};
#[allow(dead_code)]
pub enum VmError { pub enum VmError {
Catchable(String), Catchable(String),
Uncatchable(Box<Error>), Uncatchable(Box<Error>),
@@ -51,7 +50,6 @@ pub enum Break {
pub type Step = ControlFlow<Break>; pub type Step = ControlFlow<Break>;
#[allow(dead_code)]
pub struct ErrorFrame { pub struct ErrorFrame {
pub span_id: u32, pub span_id: u32,
pub message: Option<String>, pub message: Option<String>,
@@ -61,8 +59,9 @@ pub struct ErrorFrame {
#[collect(no_drop)] #[collect(no_drop)]
pub struct CallFrame<'gc> { pub struct CallFrame<'gc> {
pub pc: usize, pub pc: usize,
pub thunk: Option<Gc<'gc, Thunk<'gc>>>,
pub env: GcEnv<'gc>, pub env: GcEnv<'gc>,
pub thunk: Option<Gc<'gc, Thunk<'gc>>>,
pub depth: Option<usize>,
} }
#[derive(Debug)] #[derive(Debug)]
+7 -3
View File
@@ -31,10 +31,10 @@ impl StringContextElem {
drv_path: drv_path.into(), drv_path: drv_path.into(),
} }
} else if let Some(rest) = encoded.strip_prefix('!') { } else if let Some(rest) = encoded.strip_prefix('!') {
if let Some(second_bang) = rest.find('!') { if let Some((output, drv_path)) = rest.split_once('!') {
Self::Built { Self::Built {
output: rest[..second_bang].into(), output: output.into(),
drv_path: rest[second_bang + 1..].into(), drv_path: drv_path.into(),
} }
} else { } else {
Self::Opaque { Self::Opaque {
@@ -117,6 +117,10 @@ impl StringContext {
} }
} }
#[expect(
clippy::indexing_slicing,
reason = "`i`/`j` stay strictly below their lengths inside the loop, and the trailing slices use those in-bounds cursors as start indices"
)]
pub fn merge(&self, other: &Self) -> Self { pub fn merge(&self, other: &Self) -> Self {
if self.data.is_empty() { if self.data.is_empty() {
return other.clone(); return other.clone();
+233 -180
View File
@@ -4,7 +4,9 @@ use std::marker::PhantomData;
use std::mem::size_of; use std::mem::size_of;
use std::ops::Deref; use std::ops::Deref;
use fix_bytecode::Continuation;
use fix_lang::*; use fix_lang::*;
use fix_macros::unelide_lifetimes;
use gc_arena::barrier::Unlock; use gc_arena::barrier::Unlock;
use gc_arena::collect::Trace; use gc_arena::collect::Trace;
use gc_arena::{Collect, Gc, GcRefLock, Mutation, RefLock}; use gc_arena::{Collect, Gc, GcRefLock, Mutation, RefLock};
@@ -19,33 +21,99 @@ mod private {
pub trait Cealed {} pub trait Cealed {}
} }
/// # Safety pub trait ValueVariant<'gc>: private::Cealed {
/// #[expect(
/// [`Self::TAG`] must be unique among all implementors. private_bounds,
unsafe trait Storable: private::Cealed { reason = "Storable is a sealed implementation detail of the value system"
const TAG: RawTag; )]
type Ty: Storable<'gc>;
const TYPE: NixType;
}
trait Storable<'gc>: TryFrom<Value<'gc>> + Into<Value<'gc>> + private::Cealed + 'gc {
fn is(raw: &RawBox) -> bool;
fn to_raw_box(self) -> RawBox;
/// # Safety
///
/// `raw` must represent a valid `Self`.
unsafe fn from_raw_box(raw: RawBox) -> Self;
} }
trait InlineStorable: Storable + RawStore {}
trait GcStorable: Storable {}
macro_rules! define_value_types { macro_rules! define_value_types {
( (
inline { $($itype:ty => $itag:expr, $iname:literal;)* } inline { $($itype:ty => $itag:path, $ity:path, $iname:literal;)* }
gc { $($gtype:ty => $gtag:expr, $gname:literal;)* } gc { $($gtype:ty => $gtag:path, $gty:path, $gname:literal;)* }
) => { ) => {
$( $(
unsafe impl Storable for $itype { impl Storable<'_> for $itype {
const TAG: RawTag = $itag; #[inline(always)]
fn is(value: &RawBox) -> bool {
value.tag() == Some($itag)
}
#[inline(always)]
fn to_raw_box(self) -> RawBox {
RawBox::from_value(RawValue::store($itag, self))
}
#[inline(always)]
unsafe fn from_raw_box(raw: RawBox) -> Self {
// SAFETY: the caller guarantees `raw` represents a valid
// `Self` of this inline type, so it holds a `Value` (making
// `value()` `Some`) whose payload decodes to this type.
unsafe { <Self as RawStore>::from_val(raw.value().unwrap_unchecked()) }
}
} }
impl InlineStorable for $itype {}
impl private::Cealed for $itype {} impl private::Cealed for $itype {}
impl<'gc> ValueVariant<'gc> for $itype {
type Ty = $itype;
const TYPE: NixType = $ity;
}
impl<'gc> TryFrom<Value<'gc>> for $itype {
type Error = ();
fn try_from(val: Value<'gc>) -> Result<Self, Self::Error> {
<Self as Storable>::is(&val.raw)
// SAFETY: `is` returned true, so `val.raw` represents a
// valid `Self` of this GC type.
.then(|| unsafe { <Self as Storable>::from_raw_box(val.raw) })
.ok_or(())
}
}
)* )*
$( $(
unsafe impl Storable for $gtype { impl<'gc> Storable<'gc> for Gc<'gc, unelide_lifetimes!('gc; $gtype)> {
const TAG: RawTag = $gtag; #[inline(always)]
fn is(value: &RawBox) -> bool {
value.tag() == Some($gtag)
}
#[inline(always)]
fn to_raw_box(self) -> RawBox {
RawBox::from_value(RawValue::store($gtag, Gc::as_ptr(self)))
}
#[inline(always)]
unsafe fn from_raw_box(raw: RawBox) -> Self {
// SAFETY: the caller guarantees `raw` represents a valid
// `Self` of this GC type, so it holds a `Value` (making
// `value()` `Some`) whose payload is the pointer originally
// produced by `Gc::as_ptr` in `to_raw_box`.
unsafe { Gc::from_ptr(<*mut $gtype as RawStore>::from_val(raw.value().unwrap_unchecked())) }
}
} }
impl GcStorable for $gtype {} impl<'gc> TryFrom<Value<'gc>> for Gc<'gc, unelide_lifetimes!('gc; $gtype)> {
type Error = ();
fn try_from(val: Value<'gc>) -> Result<Self, Self::Error> {
<Self as Storable>::is(&val.raw)
// SAFETY: `is` returned true, so `val.raw` represents a
// valid `Self` of this GC type.
.then(|| unsafe { <Self as Storable>::from_raw_box(val.raw) })
.ok_or(())
}
}
impl private::Cealed for Gc<'_, $gtype> {}
impl private::Cealed for $gtype {} impl private::Cealed for $gtype {}
impl<'gc> ValueVariant<'gc> for unelide_lifetimes!('gc; $gtype) {
type Ty = Gc<'gc, unelide_lifetimes!('gc; $gtype)>;
const TYPE: NixType = $gty;
}
)* )*
const _: () = assert!(size_of::<Value<'static>>() == 8); const _: () = assert!(size_of::<Value<'static>>() == 8);
@@ -57,6 +125,7 @@ macro_rules! define_value_types {
let mut mask_true: u8 = 0; let mut mask_true: u8 = 0;
let mut i = 0; let mut i = 0;
while i < tags.len() { while i < tags.len() {
#[expect(clippy::indexing_slicing, reason = "loop condition guarantees `i < tags.len()`")]
let (neg, val) = tags[i]; let (neg, val) = tags[i];
let bit = 1 << val; let bit = 1 << val;
if neg { if neg {
@@ -70,15 +139,20 @@ macro_rules! define_value_types {
} }
}; };
// SAFETY: `trace` visits every reachable `Gc` pointer: for each GC
// tag it downcasts to the concrete `Gc` type and forwards `trace`,
// while inline tags hold no GC pointers and need no tracing.
unsafe impl<'gc> Collect<'gc> for Value<'gc> { unsafe impl<'gc> Collect<'gc> for Value<'gc> {
const NEEDS_TRACE: bool = true; const NEEDS_TRACE: bool = true;
fn trace<T: Trace<'gc>>(&self, cc: &mut T) { fn trace<T: Trace<'gc>>(&self, cc: &mut T) {
let Some(tag) = self.raw.tag() else { return }; let Some(tag) = self.raw.tag() else { return };
match tag { match tag {
$(<$gtype as Storable>::TAG => unsafe { $($gtag => unsafe {
self.load_gc::<$gtype>().trace(cc) // SAFETY: `tag` matched `$gtag`, so `downcast` to the
// corresponding GC type is guaranteed to be `Some`.
self.downcast::<$gtype>().unwrap_unchecked().trace(cc)
},)* },)*
$(<$itype as Storable>::TAG => (),)* $($itag => (),)*
_ => unreachable!("invalid value tag"), _ => unreachable!("invalid value tag"),
} }
} }
@@ -87,13 +161,17 @@ macro_rules! define_value_types {
impl fmt::Debug for Value<'_> { impl fmt::Debug for Value<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.tag() { match self.tag() {
// SAFETY: `tag()` is `None`, meaning the `RawBox` holds a
// float, so `float()` is guaranteed to be `Some`.
None => write!(f, "Float({:?})", unsafe { None => write!(f, "Float({:?})", unsafe {
self.raw.float().unwrap_unchecked() self.raw.float().unwrap_unchecked()
}), }),
$(Some(<$itype as Storable>::TAG) => write!(f, "{}({:?})", $iname, unsafe { // SAFETY: `tag()` matched `$itag`, so `downcast` to the
self.as_inline::<$itype>().unwrap_unchecked() // corresponding inline type is guaranteed to be `Some`.
$(Some($itag) => write!(f, "{}({:?})", $iname, unsafe {
self.downcast::<$itype>().unwrap_unchecked()
}),)* }),)*
$(Some(<$gtype as Storable>::TAG) => $(Some($gtag) =>
write!(f, "{}(..)", $gname),)* write!(f, "{}(..)", $gname),)*
_ => unreachable!("invalid value tag"), _ => unreachable!("invalid value tag"),
} }
@@ -104,21 +182,58 @@ macro_rules! define_value_types {
define_value_types! { define_value_types! {
inline { inline {
i32 => RawTag::P1, "SmallInt"; i32 => RawTag::P1, NixType::Int, "SmallInt";
bool => RawTag::P2, "Bool"; bool => RawTag::P2, NixType::Bool, "Bool";
Null => RawTag::P3, "Null"; Null => RawTag::P3, NixType::Null, "Null";
StringId => RawTag::P4, "SmallString"; StringId => RawTag::P4, NixType::String, "SmallString";
PrimOp => RawTag::P5, "PrimOp"; PrimOp => RawTag::P5, NixType::PrimOp, "PrimOp";
Path => RawTag::P6, "Path"; Path => RawTag::P6, NixType::Path, "Path";
} }
gc { gc {
i64 => RawTag::P7, "BigInt"; i64 => RawTag::P7, NixType::Int, "BigInt";
NixString => RawTag::N1, "String"; NixString => RawTag::N1, NixType::String, "String";
AttrSet<'_> => RawTag::N2, "AttrSet"; AttrSet<'_> => RawTag::N2, NixType::AttrSet, "AttrSet";
List<'_> => RawTag::N3, "List"; List<'_> => RawTag::N3, NixType::List, "List";
Thunk<'_> => RawTag::N4, "Thunk"; Thunk<'_> => RawTag::N4, NixType::Thunk, "Thunk";
Closure<'_> => RawTag::N5, "Closure"; Closure<'_> => RawTag::N5, NixType::Closure, "Closure";
PrimOpApp<'_> => RawTag::N6, "PrimOpApp"; PrimOpApp<'_> => RawTag::N6, NixType::PrimOpApp, "PrimOpApp";
}
}
impl private::Cealed for f64 {}
impl Storable<'_> for f64 {
#[inline(always)]
fn is(value: &RawBox) -> bool {
value.is_float()
}
#[inline(always)]
fn to_raw_box(self) -> RawBox {
RawBox::from_float(self)
}
#[inline(always)]
unsafe fn from_raw_box(raw: RawBox) -> Self {
// SAFETY: the caller guarantees `raw` represents a valid `f64`, so
// `float()` is guaranteed to be `Some`.
unsafe { raw.float().copied().unwrap_unchecked() }
}
}
impl<'gc> ValueVariant<'gc> for f64 {
type Ty = f64;
const TYPE: NixType = NixType::Float;
}
impl<'gc> TryFrom<Value<'gc>> for f64 {
type Error = ();
fn try_from(value: Value<'gc>) -> Result<Self, Self::Error> {
value.downcast::<f64>().ok_or(())
}
}
impl<'gc, T: Storable<'gc>> From<T> for Value<'gc> {
fn from(value: T) -> Self {
Value::new(value)
} }
} }
@@ -135,33 +250,11 @@ pub struct Value<'gc> {
impl Default for Value<'_> { impl Default for Value<'_> {
#[inline(always)] #[inline(always)]
fn default() -> Self { fn default() -> Self {
Self::new_inline(Null) Self::new(Null)
} }
} }
impl<'gc> Value<'gc> { impl<'gc> Value<'gc> {
#[inline(always)]
fn from_raw_value(rv: RawValue) -> Self {
Self {
raw: RawBox::from_value(rv),
_marker: PhantomData,
}
}
/// Load a GC pointer from a value with a negative tag.
///
/// # Safety
///
/// The value must actually store a `Gc<'gc, T>` with the matching type.
#[inline(always)]
unsafe fn load_gc<T: GcStorable>(self) -> Gc<'gc, T> {
unsafe {
let rv = self.raw.value().unwrap_unchecked();
let ptr: *const T = <*const T as RawStore>::from_val(rv);
Gc::from_ptr(ptr)
}
}
#[inline(always)] #[inline(always)]
const fn tag(self) -> Option<RawTag> { const fn tag(self) -> Option<RawTag> {
self.raw.tag() self.raw.tag()
@@ -170,80 +263,37 @@ impl<'gc> Value<'gc> {
impl<'gc> Value<'gc> { impl<'gc> Value<'gc> {
#[inline] #[inline]
pub fn new_float(val: f64) -> Self { #[expect(
private_bounds,
reason = "Storable is a sealed implementation detail of the value system"
)]
pub fn new<T: Storable<'gc>>(val: T) -> Self {
Self { Self {
raw: RawBox::from_float(val), raw: val.to_raw_box(),
_marker: PhantomData, _marker: PhantomData,
} }
} }
#[inline]
#[allow(private_bounds)]
pub fn new_inline<T: InlineStorable>(val: T) -> Self {
Self::from_raw_value(RawValue::store(T::TAG, val))
}
#[inline]
#[allow(private_bounds)]
pub fn new_gc<T: GcStorable>(gc: Gc<'gc, T>) -> Self {
let ptr = Gc::as_ptr(gc);
Self::from_raw_value(RawValue::store(T::TAG, ptr))
}
#[inline] #[inline]
pub fn make_int(val: i64, mc: &Mutation<'gc>) -> Self { pub fn make_int(val: i64, mc: &Mutation<'gc>) -> Self {
if val >= i32::MIN as i64 && val <= i32::MAX as i64 { if val >= i32::MIN as i64 && val <= i32::MAX as i64 {
Value::new_inline(val as i32) Value::new(val as i32)
} else { } else {
Value::new_gc(Gc::new(mc, val)) Value::new(Gc::new(mc, val))
}
}
}
impl<'gc> Value<'gc> {
#[inline]
pub fn is_float(self) -> bool {
self.raw.is_float()
}
#[inline]
#[allow(private_bounds)]
pub fn is<T: Storable>(self) -> bool {
self.tag() == Some(T::TAG)
}
}
impl<'gc> Value<'gc> {
#[inline]
pub fn as_float(self) -> Option<f64> {
self.raw.float().copied()
}
#[inline]
#[allow(private_bounds)]
pub fn as_inline<T: InlineStorable>(self) -> Option<T> {
if self.is::<T>() {
Some(unsafe {
let rv = self.raw.value().unwrap_unchecked();
T::from_val(rv)
})
} else {
None
} }
} }
#[inline] #[inline]
#[allow(private_bounds)] pub fn is<T: ValueVariant<'gc>>(self) -> bool {
pub fn as_gc<T: GcStorable>(self) -> Option<Gc<'gc, T>> { T::Ty::is(&self.raw)
if self.is::<T>() { }
Some(unsafe {
let rv = self.raw.value().unwrap_unchecked(); #[inline]
let ptr: *const T = <*const T as RawStore>::from_val(rv); pub fn downcast<T: ValueVariant<'gc>>(self) -> Option<T::Ty> {
Gc::from_ptr(ptr) self.is::<T>()
}) // SAFETY: `is::<T>()` returned true, so `self.raw` represents a
} else { // valid `T::Ty`.
None .then(|| unsafe { T::Ty::from_raw_box(self.raw) })
}
} }
#[inline] #[inline]
@@ -252,19 +302,19 @@ impl<'gc> Value<'gc> {
} }
#[inline] #[inline]
pub fn as_num(self) -> Option<NixNum> { pub fn downcast_num(self) -> Option<NixNum> {
if let Some(i) = self.as_inline::<i32>() { if let Some(i) = self.downcast::<i32>() {
Some(NixNum::Int(i as i64)) Some(NixNum::Int(i as i64))
} else if let Some(gc_i) = self.as_gc::<i64>() { } else if let Some(gc_i) = self.downcast::<i64>() {
Some(NixNum::Int(*gc_i)) Some(NixNum::Int(*gc_i))
} else { } else {
self.as_float().map(NixNum::Float) self.downcast::<f64>().map(NixNum::Float)
} }
} }
#[inline] #[inline]
pub fn restrict(self) -> Result<StrictValue<'gc>, Gc<'gc, Thunk<'gc>>> { pub fn restrict(self) -> Result<StrictValue<'gc>, Gc<'gc, Thunk<'gc>>> {
if let Some(thunk) = self.as_gc::<Thunk<'gc>>() { if let Some(thunk) = self.downcast::<Thunk>() {
Err(thunk) Err(thunk)
} else { } else {
Ok(StrictValue(self)) Ok(StrictValue(self))
@@ -272,8 +322,12 @@ impl<'gc> Value<'gc> {
} }
#[inline] #[inline]
#[expect(
clippy::unreachable,
reason = "the preceding `if`/`else if` chain exhausts every registered value tag"
)]
pub fn ty(self) -> NixType { pub fn ty(self) -> NixType {
if self.is_float() { if self.is::<f64>() {
NixType::Float NixType::Float
} else if self.is::<i32>() || self.is::<i64>() { } else if self.is::<i32>() || self.is::<i64>() {
NixType::Int NixType::Int
@@ -305,30 +359,13 @@ impl<'gc> Value<'gc> {
} }
#[inline] #[inline]
#[allow(private_bounds)] pub fn expect<T: ValueVariant<'gc>>(self) -> Result<T::Ty, NixType> {
pub fn expect_inline<T: InlineStorable>(self) -> Result<T, NixType> { self.downcast::<T>().ok_or_else(|| self.ty())
self.as_inline::<T>().ok_or_else(|| self.ty())
}
#[inline]
#[allow(private_bounds)]
pub fn expect_gc<T: GcStorable>(self) -> Result<Gc<'gc, T>, NixType> {
self.as_gc::<T>().ok_or_else(|| self.ty())
} }
#[inline] #[inline]
pub fn expect_num(self) -> Result<NixNum, NixType> { pub fn expect_num(self) -> Result<NixNum, NixType> {
self.as_num().ok_or_else(|| self.ty()) self.downcast_num().ok_or_else(|| self.ty())
}
#[inline]
pub fn expect_bool(self) -> Result<bool, NixType> {
self.as_inline::<bool>().ok_or_else(|| self.ty())
}
#[inline]
pub fn expect_float(self) -> Result<f64, NixType> {
self.as_float().ok_or_else(|| self.ty())
} }
} }
@@ -346,40 +383,24 @@ impl<'gc> From<StaticValue> for Value<'gc> {
impl StaticValue { impl StaticValue {
#[inline] #[inline]
pub fn new_float(val: f64) -> Self { #[expect(
Self(Value::new_float(val)) private_bounds,
reason = "Storable is a sealed implementation detail of the value system"
)]
pub fn new<T: Storable<'static>>(val: T) -> Self {
Self(Value::new(val))
} }
#[inline] #[inline]
#[allow(private_bounds)] pub fn is<T: ValueVariant<'static>>(self) -> bool {
pub fn new_inline<T: InlineStorable>(val: T) -> Self {
Self(Value::new_inline(val))
}
#[inline]
pub fn new_primop(id: BuiltinId, arity: u8, dispatch_ip: u32) -> Self {
Self(Value::new_inline(PrimOp {
id,
arity,
dispatch_ip,
}))
}
#[inline]
pub fn is_float(self) -> bool {
self.0.is_float()
}
#[inline]
#[allow(private_bounds)]
pub fn is<T: InlineStorable>(self) -> bool {
self.0.is::<T>() self.0.is::<T>()
} }
#[inline] #[inline]
pub fn as_float(self) -> Option<f64> { pub fn downcast<T: ValueVariant<'static>>(self) -> Option<T::Ty> {
self.0.as_float() self.0.downcast::<T>()
}
#[inline]
#[allow(private_bounds)]
pub fn as_inline<T: InlineStorable>(self) -> Option<T> {
self.0.as_inline::<T>()
} }
#[inline] #[inline]
pub fn to_bits(self) -> u64 { pub fn to_bits(self) -> u64 {
self.0.raw.to_bits() self.0.raw.to_bits()
@@ -478,6 +499,10 @@ impl<'gc> AttrSet<'gc> {
Self { entries } Self { entries }
} }
#[expect(
clippy::indexing_slicing,
reason = "index comes from a successful `binary_search_by_key`, so it is a valid entry index"
)]
pub fn lookup(&self, key: StringId) -> Option<Value<'gc>> { pub fn lookup(&self, key: StringId) -> Option<Value<'gc>> {
self.entries self.entries
.binary_search_by_key(&key, |(k, _)| *k) .binary_search_by_key(&key, |(k, _)| *k)
@@ -489,6 +514,10 @@ impl<'gc> AttrSet<'gc> {
self.entries.binary_search_by_key(&key, |(k, _)| *k).is_ok() self.entries.binary_search_by_key(&key, |(k, _)| *k).is_ok()
} }
#[expect(
clippy::indexing_slicing,
reason = "`i`/`j` stay strictly below their lengths inside the loop, and the trailing slices use those in-bounds cursors as start indices"
)]
pub fn merge(&self, other: &Self, mc: &Mutation<'gc>) -> Gc<'gc, Self> { pub fn merge(&self, other: &Self, mc: &Mutation<'gc>) -> Gc<'gc, Self> {
use std::cmp::Ordering::*; use std::cmp::Ordering::*;
@@ -549,6 +578,9 @@ impl<'gc> List<'gc> {
impl<'gc> Unlock for List<'gc> { impl<'gc> Unlock for List<'gc> {
type Unlocked = RefCell<SmallVec<[Value<'gc>; 4]>>; type Unlocked = RefCell<SmallVec<[Value<'gc>; 4]>>;
unsafe fn unlock_unchecked(&self) -> &Self::Unlocked { unsafe fn unlock_unchecked(&self) -> &Self::Unlocked {
// SAFETY: the caller upholds the `Unlock` contract (mutation happens
// behind a write barrier); we forward that obligation to the inner
// `RefLock`'s `unlock_unchecked`.
unsafe { self.inner.unlock_unchecked() } unsafe { self.inner.unlock_unchecked() }
} }
} }
@@ -572,14 +604,6 @@ pub struct Env<'gc> {
} }
pub type GcEnv<'gc> = GcRefLock<'gc, Env<'gc>>; pub type GcEnv<'gc> = GcRefLock<'gc, Env<'gc>>;
#[derive(Collect, Debug)]
#[collect(no_drop)]
pub struct WithEnv<'gc> {
pub env: Value<'gc>,
pub prev: Option<GcWithEnv<'gc>>,
}
pub type GcWithEnv<'gc> = Gc<'gc, WithEnv<'gc>>;
impl<'gc> Env<'gc> { impl<'gc> Env<'gc> {
pub fn empty() -> Self { pub fn empty() -> Self {
Env { Env {
@@ -588,6 +612,10 @@ impl<'gc> Env<'gc> {
} }
} }
#[expect(
clippy::indexing_slicing,
reason = "`locals` was just created with `1 + n_locals` elements, so index 0 is always valid"
)]
pub fn with_arg(arg: Value<'gc>, n_locals: u32, prev: Gc<'gc, RefLock<Env<'gc>>>) -> Self { pub fn with_arg(arg: Value<'gc>, n_locals: u32, prev: Gc<'gc, RefLock<Env<'gc>>>) -> Self {
let mut locals = smallvec::smallvec![Value::default(); 1 + n_locals as usize]; let mut locals = smallvec::smallvec![Value::default(); 1 + n_locals as usize];
locals[0] = arg; locals[0] = arg;
@@ -625,6 +653,18 @@ pub struct PrimOp {
pub dispatch_ip: u32, pub dispatch_ip: u32,
} }
impl From<BuiltinId> for PrimOp {
fn from(id: BuiltinId) -> Self {
let BuiltinInfo { arity, .. } = id.info();
let dispatch_ip = Continuation::entry_for_builtin(id).ip();
Self {
id,
arity,
dispatch_ip,
}
}
}
impl RawStore for PrimOp { impl RawStore for PrimOp {
fn to_val(self, value: &mut RawValue) { fn to_val(self, value: &mut RawValue) {
let bytes = self.dispatch_ip.to_le_bytes(); let bytes = self.dispatch_ip.to_le_bytes();
@@ -675,6 +715,19 @@ impl<'gc> Deref for StrictValue<'gc> {
} }
} }
impl<'gc> From<StrictValue<'gc>> for Value<'gc> {
fn from(value: StrictValue<'gc>) -> Self {
value.0
}
}
impl<'gc> TryFrom<Value<'gc>> for StrictValue<'gc> {
type Error = Gc<'gc, Thunk<'gc>>;
fn try_from(value: Value<'gc>) -> Result<Self, Self::Error> {
value.restrict()
}
}
impl fmt::Debug for StrictValue<'_> { impl fmt::Debug for StrictValue<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Debug::fmt(&self.0, f) fmt::Debug::fmt(&self.0, f)
+5 -1
View File
@@ -7,12 +7,16 @@ edition = "2024"
gc-arena = { workspace = true } gc-arena = { workspace = true }
hashbrown = { workspace = true } hashbrown = { workspace = true }
smallvec = { workspace = true } smallvec = { workspace = true }
sysinfo = { version = "0.38", default-features = false, features = ["system"] } sysinfo = { version = "0.39", default-features = false, features = ["system"] }
fix-bytecode = { path = "../fix-bytecode" } fix-bytecode = { path = "../fix-bytecode" }
fix-error = { path = "../fix-error" } fix-error = { path = "../fix-error" }
fix-lang = { path = "../fix-lang" } fix-lang = { path = "../fix-lang" }
fix-macros = { path = "../fix-macros" }
fix-runtime = { path = "../fix-runtime" } fix-runtime = { path = "../fix-runtime" }
[features] [features]
tailcall = [] tailcall = []
[lints]
workspace = true
+6 -1
View File
@@ -22,6 +22,7 @@ pub(crate) type OpFn<'gc, C> = extern "rust-preserve-none" fn(
pub(crate) struct DispatchTable<'gc, C: VmRuntimeCtx>(pub(crate) [OpFn<'gc, C>; 256]); pub(crate) struct DispatchTable<'gc, C: VmRuntimeCtx>(pub(crate) [OpFn<'gc, C>; 256]);
#[expect(clippy::panic, reason = "illegal opcode should panic")]
extern "rust-preserve-none" fn op_illegal<'gc, C: VmRuntimeCtx>( extern "rust-preserve-none" fn op_illegal<'gc, C: VmRuntimeCtx>(
_vm: &mut Vm<'gc>, _vm: &mut Vm<'gc>,
_mc: &Mutation<'gc>, _mc: &Mutation<'gc>,
@@ -200,6 +201,7 @@ tail_fn!(op_load_scoped_binding, (ctx, reader, mc));
macro_rules! table { macro_rules! table {
($($variant:ident => $fn:ident),* $(,)?) => { ($($variant:ident => $fn:ident),* $(,)?) => {
impl<'gc, C: VmRuntimeCtx> DispatchTable<'gc, C> { impl<'gc, C: VmRuntimeCtx> DispatchTable<'gc, C> {
#[expect(clippy::indexing_slicing, reason = "Op is repr(u8)")]
pub(crate) const NEW: Self = { pub(crate) const NEW: Self = {
let mut arr: [OpFn<'gc, C>; 256] = [op_illegal; 256]; let mut arr: [OpFn<'gc, C>; 256] = [op_illegal; 256];
$( arr[fix_bytecode::Op::$variant as usize] = $fn; )* $( arr[fix_bytecode::Op::$variant as usize] = $fn; )*
@@ -209,7 +211,6 @@ macro_rules! table {
// Exhaustiveness check: fails to compile if `fix_bytecode::Op` gains, // Exhaustiveness check: fails to compile if `fix_bytecode::Op` gains,
// loses, or renames a variant that isn't wired up above. // loses, or renames a variant that isn't wired up above.
#[allow(dead_code)]
const _: fn(fix_bytecode::Op) = |op| match op { const _: fn(fix_bytecode::Op) = |op| match op {
$( fix_bytecode::Op::$variant => (), )* $( fix_bytecode::Op::$variant => (), )*
}; };
@@ -291,6 +292,10 @@ table! {
Illegal => op_illegal, Illegal => op_illegal,
} }
#[expect(
clippy::indexing_slicing,
reason = "assume well-formed bytecode; Op is repr(u8)"
)]
pub(crate) fn run_tailcall<'gc, C: VmRuntimeCtx>( pub(crate) fn run_tailcall<'gc, C: VmRuntimeCtx>(
vm: &mut Vm<'gc>, vm: &mut Vm<'gc>,
mc: &Mutation<'gc>, mc: &Mutation<'gc>,
+16 -20
View File
@@ -28,7 +28,7 @@ pub(crate) fn op_add<'gc, M: Machine<'gc>>(
let combined = format!("{ls}{rs}"); let combined = format!("{ls}{rs}");
let canon = canon_path_str(&combined); let canon = canon_path_str(&combined);
let sid = ctx.intern_string(canon); let sid = ctx.intern_string(canon);
m.push(Value::new_inline(fix_runtime::Path(sid))); m.push(Value::new(fix_runtime::Path(sid)));
return Step::Continue(()); return Step::Continue(());
} }
if let (Some(ls), Some(rs)) = (ctx.get_string(lhs), ctx.get_string_or_path(rhs)) { if let (Some(ls), Some(rs)) = (ctx.get_string(lhs), ctx.get_string_or_path(rhs)) {
@@ -39,7 +39,7 @@ pub(crate) fn op_add<'gc, M: Machine<'gc>>(
mc, mc,
crate::NixString::with_context(format!("{ls}{rs}"), merged), crate::NixString::with_context(format!("{ls}{rs}"), merged),
); );
m.push(Value::new_gc(ns)); m.push(Value::new(ns));
return Step::Continue(()); return Step::Continue(());
} }
let res = numeric_binop(lhs, rhs, mc, i64::wrapping_add, |a, b| a + b); let res = numeric_binop(lhs, rhs, mc, i64::wrapping_add, |a, b| a + b);
@@ -200,7 +200,7 @@ pub(crate) fn op_concat<'gc, M: Machine<'gc>>(
let mut items = smallvec::SmallVec::new(); let mut items = smallvec::SmallVec::new();
items.extend_from_slice(&l.inner.borrow()); items.extend_from_slice(&l.inner.borrow());
items.extend_from_slice(&r.inner.borrow()); items.extend_from_slice(&r.inner.borrow());
m.push(Value::new_gc(Gc::new( m.push(Value::new(Gc::new(
mc, mc,
crate::List { crate::List {
inner: RefLock::new(items), inner: RefLock::new(items),
@@ -216,7 +216,7 @@ pub(crate) fn op_update<'gc, M: Machine<'gc>>(
mc: &Mutation<'gc>, mc: &Mutation<'gc>,
) -> Step { ) -> Step {
let (l, r) = m.force_and_retry::<(Gc<AttrSet>, Gc<AttrSet>)>(reader, mc)?; let (l, r) = m.force_and_retry::<(Gc<AttrSet>, Gc<AttrSet>)>(reader, mc)?;
m.push(Value::new_gc(l.merge(&r, mc))); m.push(Value::new(l.merge(&r, mc)));
Step::Continue(()) Step::Continue(())
} }
@@ -229,7 +229,7 @@ pub(crate) fn op_neg<'gc, M: Machine<'gc>>(
let rhs = m.force_and_retry::<NixNum>(reader, mc)?; let rhs = m.force_and_retry::<NixNum>(reader, mc)?;
match rhs { match rhs {
NixNum::Int(int) => m.push(Value::make_int(-int, mc)), NixNum::Int(int) => m.push(Value::make_int(-int, mc)),
NixNum::Float(float) => m.push(Value::new_float(-float)), NixNum::Float(float) => m.push(Value::new(-float)),
} }
Step::Continue(()) Step::Continue(())
} }
@@ -241,7 +241,7 @@ pub(crate) fn op_not<'gc, M: Machine<'gc>>(
mc: &Mutation<'gc>, mc: &Mutation<'gc>,
) -> Step { ) -> Step {
let rhs = m.force_and_retry::<bool>(reader, mc)?; let rhs = m.force_and_retry::<bool>(reader, mc)?;
m.push(Value::new_inline(!rhs)); m.push(Value::new(!rhs));
Step::Continue(()) Step::Continue(())
} }
@@ -263,17 +263,17 @@ fn compare_values_inner<'gc, M: Machine<'gc>>(
a.partial_cmp(&(b as f64)).unwrap_or(Ordering::Less) a.partial_cmp(&(b as f64)).unwrap_or(Ordering::Less)
} }
}; };
m.push(Value::new_inline(pred(ord))); m.push(Value::new(pred(ord)));
return Ok(()); return Ok(());
} }
if let (Some(a), Some(b)) = (ctx.get_string(lhs), ctx.get_string(rhs)) { if let (Some(a), Some(b)) = (ctx.get_string(lhs), ctx.get_string(rhs)) {
m.push(Value::new_inline(pred(a.cmp(b)))); m.push(Value::new(pred(a.cmp(b))));
return Ok(()); return Ok(());
} }
if let (Some(a), Some(b)) = (lhs.as_inline::<Path>(), rhs.as_inline::<Path>()) { if let (Some(a), Some(b)) = (lhs.downcast::<Path>(), rhs.downcast::<Path>()) {
let a = ctx.resolve_string(a.0); let a = ctx.resolve_string(a.0);
let b = ctx.resolve_string(b.0); let b = ctx.resolve_string(b.0);
m.push(Value::new_inline(pred(a.cmp(b)))); m.push(Value::new(pred(a.cmp(b))));
return Ok(()); return Ok(());
} }
// TODO: compare other types // TODO: compare other types
@@ -285,12 +285,12 @@ fn compare_values_inner<'gc, M: Machine<'gc>>(
} }
pub(crate) fn get_num(val: StrictValue<'_>) -> Option<NixNum> { pub(crate) fn get_num(val: StrictValue<'_>) -> Option<NixNum> {
if let Some(i) = val.as_inline::<i32>() { if let Some(i) = val.downcast::<i32>() {
Some(NixNum::Int(i64::from(i))) Some(NixNum::Int(i64::from(i)))
} else if let Some(gc_i) = val.as_gc::<i64>() { } else if let Some(gc_i) = val.downcast::<i64>() {
Some(NixNum::Int(*gc_i)) Some(NixNum::Int(*gc_i))
} else { } else {
val.as_float().map(NixNum::Float) val.downcast::<f64>().map(NixNum::Float)
} }
} }
@@ -304,13 +304,9 @@ fn numeric_binop<'gc>(
) -> crate::VmResult<Value<'gc>> { ) -> crate::VmResult<Value<'gc>> {
match (get_num(lhs), get_num(rhs)) { match (get_num(lhs), get_num(rhs)) {
(Some(NixNum::Int(a)), Some(NixNum::Int(b))) => Ok(Value::make_int(int_op(a, b), mc)), (Some(NixNum::Int(a)), Some(NixNum::Int(b))) => Ok(Value::make_int(int_op(a, b), mc)),
(Some(NixNum::Float(a)), Some(NixNum::Float(b))) => Ok(Value::new_float(float_op(a, b))), (Some(NixNum::Float(a)), Some(NixNum::Float(b))) => Ok(Value::new(float_op(a, b))),
(Some(NixNum::Int(a)), Some(NixNum::Float(b))) => { (Some(NixNum::Int(a)), Some(NixNum::Float(b))) => Ok(Value::new(float_op(a as f64, b))),
Ok(Value::new_float(float_op(a as f64, b))) (Some(NixNum::Float(a)), Some(NixNum::Int(b))) => Ok(Value::new(float_op(a, b as f64))),
}
(Some(NixNum::Float(a)), Some(NixNum::Int(b))) => {
Ok(Value::new_float(float_op(a, b as f64)))
}
_ => Err(crate::vm_err(format!( _ => Err(crate::vm_err(format!(
"cannot perform arithmetic on non-numbers: {:?}", "cannot perform arithmetic on non-numbers: {:?}",
(lhs.ty(), rhs.ty()) (lhs.ty(), rhs.ty())
+21 -7
View File
@@ -9,6 +9,10 @@ use crate::{
}; };
#[inline(always)] #[inline(always)]
#[expect(
clippy::indexing_slicing,
reason = "app.args is a fixed [Value; 3]; indices are bounded by primop arity (<= 3) validated at PrimOpApp construction"
)]
pub(crate) fn call<'gc, M: Machine<'gc>>( pub(crate) fn call<'gc, M: Machine<'gc>>(
m: &mut M, m: &mut M,
reader: &mut BytecodeReader<'_>, reader: &mut BytecodeReader<'_>,
@@ -21,7 +25,7 @@ pub(crate) fn call<'gc, M: Machine<'gc>>(
return m.finish_err(Error::eval_error("stack overflow; max-call-depth exceeded")); return m.finish_err(Error::eval_error("stack overflow; max-call-depth exceeded"));
} }
m.inc_call_depth(); m.inc_call_depth();
if let Some(closure) = func.as_gc::<Closure>() { if let Some(closure) = func.downcast::<Closure>() {
if closure.pattern.is_some() { if closure.pattern.is_some() {
// FIXME: better DX... // FIXME: better DX...
m.push(func.relax()); m.push(func.relax());
@@ -30,6 +34,7 @@ pub(crate) fn call<'gc, M: Machine<'gc>>(
pc: resume_pc, pc: resume_pc,
thunk: None, thunk: None,
env: m.env(), env: m.env(),
depth: None,
}); });
reader.set_pc(Continuation::CallPattern.ip() as usize); reader.set_pc(Continuation::CallPattern.ip() as usize);
return Step::Continue(()); return Step::Continue(());
@@ -43,16 +48,18 @@ pub(crate) fn call<'gc, M: Machine<'gc>>(
pc: resume_pc, pc: resume_pc,
thunk: None, thunk: None,
env: m.env(), env: m.env(),
depth: None,
}); });
reader.set_pc(ip as usize); reader.set_pc(ip as usize);
m.set_env(new_env); m.set_env(new_env);
} else if let Some(primop) = func.as_inline::<PrimOp>() { } else if let Some(primop) = func.downcast::<PrimOp>() {
if primop.arity == 1 { if primop.arity == 1 {
m.push(arg); m.push(arg);
m.push_call_frame(CallFrame { m.push_call_frame(CallFrame {
pc: resume_pc, pc: resume_pc,
thunk: None, thunk: None,
env: m.env(), env: m.env(),
depth: None,
}); });
reader.set_pc(primop.dispatch_ip as usize) reader.set_pc(primop.dispatch_ip as usize)
} else { } else {
@@ -61,9 +68,9 @@ pub(crate) fn call<'gc, M: Machine<'gc>>(
arity: primop.arity - 1, arity: primop.arity - 1,
args: [arg, Value::default(), Value::default()], args: [arg, Value::default(), Value::default()],
}; };
m.push(Value::new_gc(Gc::new(mc, app))); m.push(Value::new(Gc::new(mc, app)));
} }
} else if let Some(app) = func.as_gc::<PrimOpApp>() { } else if let Some(app) = func.downcast::<PrimOpApp>() {
if app.arity == 1 { if app.arity == 1 {
for i in 0..app.primop.arity - 1 { for i in 0..app.primop.arity - 1 {
m.push(app.args[i as usize]); m.push(app.args[i as usize]);
@@ -73,6 +80,7 @@ pub(crate) fn call<'gc, M: Machine<'gc>>(
pc: resume_pc, pc: resume_pc,
thunk: None, thunk: None,
env: m.env(), env: m.env(),
depth: None,
}); });
reader.set_pc(app.primop.dispatch_ip as usize) reader.set_pc(app.primop.dispatch_ip as usize)
} else { } else {
@@ -82,9 +90,9 @@ pub(crate) fn call<'gc, M: Machine<'gc>>(
..*app ..*app
}; };
new_app.args[position] = arg; new_app.args[position] = arg;
m.push(Value::new_gc(Gc::new(mc, new_app))) m.push(Value::new(Gc::new(mc, new_app)))
} }
} else if let Some(attrs) = func.as_gc::<AttrSet>() } else if let Some(attrs) = func.downcast::<AttrSet>()
&& let Some(functor) = attrs.lookup(m.functor_sym()) && let Some(functor) = attrs.lookup(m.functor_sym())
{ {
// f arg => (f.__functor f) arg // f arg => (f.__functor f) arg
@@ -97,6 +105,7 @@ pub(crate) fn call<'gc, M: Machine<'gc>>(
pc: resume_pc, pc: resume_pc,
thunk: None, thunk: None,
env: m.env(), env: m.env(),
depth: None,
}); });
m.push(arg); m.push(arg);
m.push(func.relax()); m.push(func.relax());
@@ -136,6 +145,7 @@ pub(crate) fn op_return<'gc, M: Machine<'gc>>(
pc: ret_pc, pc: ret_pc,
thunk, thunk,
env, env,
depth,
}) = m.pop_call_frame() }) = m.pop_call_frame()
else { else {
match m.force_mode() { match m.force_mode() {
@@ -152,9 +162,10 @@ pub(crate) fn op_return<'gc, M: Machine<'gc>>(
pc: Continuation::ForceResultDeepFinish.ip() as usize, pc: Continuation::ForceResultDeepFinish.ip() as usize,
thunk: None, thunk: None,
env: m.env(), env: m.env(),
depth: None,
}); });
m.inc_call_depth(); m.inc_call_depth();
reader.set_pc(Continuation::PDeepSeq.ip() as usize); reader.set_pc(Continuation::PDeepSeq0.ip() as usize);
return Step::Continue(()); return Step::Continue(());
} }
} }
@@ -162,6 +173,9 @@ pub(crate) fn op_return<'gc, M: Machine<'gc>>(
reader.set_pc(ret_pc); reader.set_pc(ret_pc);
if let Some(outer_thunk) = thunk { if let Some(outer_thunk) = thunk {
*outer_thunk.borrow_mut(mc) = ThunkState::Evaluated(val); *outer_thunk.borrow_mut(mc) = ThunkState::Evaluated(val);
if let Some(depth) = depth {
m.replace(depth, val.relax());
}
} else { } else {
m.dec_call_depth(); m.dec_call_depth();
m.push(val.relax()) m.push(val.relax())
+3 -3
View File
@@ -17,7 +17,7 @@ pub(crate) fn op_make_thunk<'gc, M: Machine<'gc>>(
env: m.env(), env: m.env(),
}), }),
); );
m.push(Value::new_gc(thunk)); m.push(Value::new(thunk));
Step::Continue(()) Step::Continue(())
} }
@@ -38,7 +38,7 @@ pub(crate) fn op_make_closure<'gc, M: Machine<'gc>>(
pattern: None, pattern: None,
}, },
); );
m.push(Value::new_gc(closure)); m.push(Value::new(closure));
Step::Continue(()) Step::Continue(())
} }
@@ -88,6 +88,6 @@ pub(crate) fn op_make_pattern_closure<'gc, M: Machine<'gc>>(
pattern: Some(pattern), pattern: Some(pattern),
}, },
); );
m.push(Value::new_gc(closure)); m.push(Value::new(closure));
Step::Continue(()) Step::Continue(())
} }
+30 -9
View File
@@ -57,7 +57,7 @@ pub(crate) fn op_make_attrs<'gc, M: Machine<'gc>>(
kv.sort_by_key(|(k, _)| *k); kv.sort_by_key(|(k, _)| *k);
let attrs = Gc::new(mc, AttrSet::from_sorted_unchecked(kv)); let attrs = Gc::new(mc, AttrSet::from_sorted_unchecked(kv));
m.push(Value::new_gc(attrs)); m.push(Value::new(attrs));
Step::Continue(()) Step::Continue(())
} }
@@ -117,6 +117,10 @@ pub(crate) fn op_select_dynamic<'gc, M: Machine<'gc>>(
/// Only recognises Select opcodes and jumps; encountering any other /// Only recognises Select opcodes and jumps; encountering any other
/// opcode means we've reached the end of the select sequence and /// opcode means we've reached the end of the select sequence and
/// should report the missing-attribute error. /// 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>>( fn select_skip<'gc, M: Machine<'gc>>(
m: &mut M, m: &mut M,
key: StringId, key: StringId,
@@ -126,6 +130,7 @@ fn select_skip<'gc, M: Machine<'gc>>(
use fix_bytecode::Op::*; use fix_bytecode::Op::*;
loop { loop {
match reader.read_op() { match reader.read_op() {
// Skip rest of the attrpath
SelectStatic => { SelectStatic => {
reader.set_pc(reader.pc() + 4 + 4); reader.set_pc(reader.pc() + 4 + 4);
} }
@@ -136,10 +141,14 @@ fn select_skip<'gc, M: Machine<'gc>>(
reader.set_pc(reader.pc() + 4); reader.set_pc(reader.pc() + 4);
break Step::Continue(()); break Step::Continue(());
} }
// Default (`a.b or c`)
JumpIfSelectFailed => { JumpIfSelectFailed => {
let offset = reader.read_i32(); let offset = reader.read_i32();
reader.set_pc(((reader.pc() as isize) + (offset as isize)) as usize); reader.set_pc(((reader.pc() as isize) + (offset as isize)) as usize);
} }
// Report error
_ => { _ => {
let name = ctx.resolve_string(key); let name = ctx.resolve_string(key);
return m.finish_err(Error::eval_error(format!("attribute '{name}' missing"))); return m.finish_err(Error::eval_error(format!("attribute '{name}' missing")));
@@ -150,6 +159,14 @@ fn select_skip<'gc, M: Machine<'gc>>(
/// Skip the rest of a **HasAttr** attrpath after an intermediate /// Skip the rest of a **HasAttr** attrpath after an intermediate
/// lookup failed. Only recognises HasAttr opcodes and jumps. /// 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 { fn has_attr_skip(reader: &mut BytecodeReader<'_>) -> Step {
use fix_bytecode::Op::*; use fix_bytecode::Op::*;
loop { loop {
@@ -195,7 +212,7 @@ pub(crate) fn op_has_attr_path_static<'gc, M: Machine<'gc>>(
let current = m.force_and_retry::<StrictValue>(reader, mc)?; let current = m.force_and_retry::<StrictValue>(reader, mc)?;
match current match current
.as_gc::<AttrSet>() .downcast::<AttrSet>()
.and_then(|attrs| attrs.lookup(key)) .and_then(|attrs| attrs.lookup(key))
{ {
Some(v) => { Some(v) => {
@@ -223,7 +240,7 @@ pub(crate) fn op_has_attr_path_dynamic<'gc, M: Machine<'gc>>(
}; };
match current match current
.as_gc::<AttrSet>() .downcast::<AttrSet>()
.and_then(|attrs| attrs.lookup(key_sid)) .and_then(|attrs| attrs.lookup(key_sid))
{ {
Some(v) => { Some(v) => {
@@ -245,6 +262,10 @@ pub(crate) fn op_jump_if_select_failed<'gc, M: Machine<'gc>>(
} }
#[inline(always)] #[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>>( pub(crate) fn op_jump_if_select_succeeded<'gc, M: Machine<'gc>>(
_m: &mut M, _m: &mut M,
reader: &mut BytecodeReader<'_>, reader: &mut BytecodeReader<'_>,
@@ -263,9 +284,9 @@ pub(crate) fn op_has_attr_static<'gc, M: Machine<'gc>>(
let key = reader.read_string_id(); let key = reader.read_string_id();
let current = m.force_and_retry::<StrictValue>(reader, mc)?; let current = m.force_and_retry::<StrictValue>(reader, mc)?;
m.push(Value::new_inline( m.push(Value::new(
current current
.as_gc::<AttrSet>() .downcast::<AttrSet>()
.and_then(|attrs| attrs.lookup(key)) .and_then(|attrs| attrs.lookup(key))
.is_some(), .is_some(),
)); ));
@@ -289,9 +310,9 @@ pub(crate) fn op_has_attr_dynamic<'gc, M: MachineExt<'gc>>(
Err(got) => return m.finish_type_err(NixType::String, got), Err(got) => return m.finish_type_err(NixType::String, got),
}; };
m.push(Value::new_inline( m.push(Value::new(
current current
.as_gc::<AttrSet>() .downcast::<AttrSet>()
.and_then(|attrs| attrs.lookup(key_sid)) .and_then(|attrs| attrs.lookup(key_sid))
.is_some(), .is_some(),
)); ));
@@ -304,7 +325,7 @@ pub(crate) fn op_has_attr_dynamic<'gc, M: MachineExt<'gc>>(
#[inline(always)] #[inline(always)]
pub(crate) fn op_has_attr_resolve<'gc, M: Machine<'gc>>(m: &mut M) -> Step { 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) // If we reach here, has_attr check has failed, push false (AttrSet is already popped)
m.push(Value::new_inline(false)); m.push(Value::new(false));
Step::Continue(()) Step::Continue(())
} }
@@ -326,7 +347,7 @@ pub(crate) fn op_make_list<'gc, M: Machine<'gc>>(
inner: RefLock::new(items), inner: RefLock::new(items),
}, },
); );
m.push(Value::new_gc(list)); m.push(Value::new(list));
Step::Continue(()) Step::Continue(())
} }
+14 -2
View File
@@ -5,6 +5,10 @@ use gc_arena::Mutation;
use crate::{BytecodeReader, Step, VmRuntimeCtx}; use crate::{BytecodeReader, Step, VmRuntimeCtx};
#[inline(always)] #[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_false<'gc, M: Machine<'gc>>( pub(crate) fn op_jump_if_false<'gc, M: Machine<'gc>>(
m: &mut M, m: &mut M,
reader: &mut BytecodeReader<'_>, reader: &mut BytecodeReader<'_>,
@@ -12,13 +16,17 @@ pub(crate) fn op_jump_if_false<'gc, M: Machine<'gc>>(
) -> Step { ) -> Step {
let offset = reader.read_i32(); let offset = reader.read_i32();
let cond = m.force_and_retry::<StrictValue>(reader, mc)?; let cond = m.force_and_retry::<StrictValue>(reader, mc)?;
if cond.as_inline::<bool>() == Some(false) { if cond.downcast::<bool>() == Some(false) {
reader.set_pc(((reader.pc() as isize) + (offset as isize)) as usize); reader.set_pc(((reader.pc() as isize) + (offset as isize)) as usize);
} }
Step::Continue(()) Step::Continue(())
} }
#[inline(always)] #[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_true<'gc, M: Machine<'gc>>( pub(crate) fn op_jump_if_true<'gc, M: Machine<'gc>>(
m: &mut M, m: &mut M,
reader: &mut BytecodeReader<'_>, reader: &mut BytecodeReader<'_>,
@@ -26,13 +34,17 @@ pub(crate) fn op_jump_if_true<'gc, M: Machine<'gc>>(
) -> Step { ) -> Step {
let offset = reader.read_i32(); let offset = reader.read_i32();
let cond = m.force_and_retry::<StrictValue>(reader, mc)?; let cond = m.force_and_retry::<StrictValue>(reader, mc)?;
if cond.as_inline::<bool>() == Some(true) { if cond.downcast::<bool>() == Some(true) {
reader.set_pc(((reader.pc() as isize) + (offset as isize)) as usize); reader.set_pc(((reader.pc() as isize) + (offset as isize)) as usize);
} }
Step::Continue(()) Step::Continue(())
} }
#[inline(always)] #[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<'gc, M: Machine<'gc>>(_m: &mut M, reader: &mut BytecodeReader<'_>) -> Step { pub(crate) fn op_jump<'gc, M: Machine<'gc>>(_m: &mut M, reader: &mut BytecodeReader<'_>) -> Step {
let offset = reader.read_i32(); let offset = reader.read_i32();
reader.set_pc(((reader.pc() as isize) + (offset as isize)) as usize); reader.set_pc(((reader.pc() as isize) + (offset as isize)) as usize);
+7 -7
View File
@@ -9,7 +9,7 @@ pub(crate) fn op_push_smi<'gc, M: Machine<'gc>>(
reader: &mut BytecodeReader<'_>, reader: &mut BytecodeReader<'_>,
) -> Step { ) -> Step {
let val = reader.read_i32(); let val = reader.read_i32();
m.push(Value::new_inline(val)); m.push(Value::new(val));
Step::Continue(()) Step::Continue(())
} }
@@ -20,7 +20,7 @@ pub(crate) fn op_push_bigint<'gc, M: Machine<'gc>>(
mc: &Mutation<'gc>, mc: &Mutation<'gc>,
) -> Step { ) -> Step {
let val = reader.read_i64(); let val = reader.read_i64();
m.push(Value::new_gc(Gc::new(mc, val))); m.push(Value::new(Gc::new(mc, val)));
Step::Continue(()) Step::Continue(())
} }
@@ -30,7 +30,7 @@ pub(crate) fn op_push_float<'gc, M: Machine<'gc>>(
reader: &mut BytecodeReader<'_>, reader: &mut BytecodeReader<'_>,
) -> Step { ) -> Step {
let val = reader.read_f64(); let val = reader.read_f64();
m.push(Value::new_float(val)); m.push(Value::new(val));
Step::Continue(()) Step::Continue(())
} }
@@ -40,24 +40,24 @@ pub(crate) fn op_push_string<'gc, M: Machine<'gc>>(
reader: &mut BytecodeReader<'_>, reader: &mut BytecodeReader<'_>,
) -> Step { ) -> Step {
let sid = reader.read_string_id(); let sid = reader.read_string_id();
m.push(Value::new_inline(sid)); m.push(Value::new(sid));
Step::Continue(()) Step::Continue(())
} }
#[inline(always)] #[inline(always)]
pub(crate) fn op_push_null<'gc, M: Machine<'gc>>(m: &mut M) -> Step { pub(crate) fn op_push_null<'gc, M: Machine<'gc>>(m: &mut M) -> Step {
m.push(Value::new_inline(crate::Null)); m.push(Value::new(crate::Null));
Step::Continue(()) Step::Continue(())
} }
#[inline(always)] #[inline(always)]
pub(crate) fn op_push_true<'gc, M: Machine<'gc>>(m: &mut M) -> Step { pub(crate) fn op_push_true<'gc, M: Machine<'gc>>(m: &mut M) -> Step {
m.push(Value::new_inline(true)); m.push(Value::new(true));
Step::Continue(()) Step::Continue(())
} }
#[inline(always)] #[inline(always)]
pub(crate) fn op_push_false<'gc, M: Machine<'gc>>(m: &mut M) -> Step { pub(crate) fn op_push_false<'gc, M: Machine<'gc>>(m: &mut M) -> Step {
m.push(Value::new_inline(false)); m.push(Value::new(false));
Step::Continue(()) Step::Continue(())
} }
+16 -12
View File
@@ -2,7 +2,7 @@ use std::path::PathBuf;
use fix_bytecode::Continuation; use fix_bytecode::Continuation;
use fix_error::Error; use fix_error::Error;
use fix_lang::{BUILTINS, BuiltinId, StringId}; use fix_lang::{BuiltinId, StringId};
use fix_runtime::{ use fix_runtime::{
AttrSet, Machine, MachineExt, NixString, Path, StrictValue, StringContext, canon_path_str, AttrSet, Machine, MachineExt, NixString, Path, StrictValue, StringContext, canon_path_str,
}; };
@@ -11,20 +11,24 @@ use crate::{BytecodeReader, PrimOp, Step, Value, VmRuntimeCtx, VmRuntimeCtxExt};
#[inline(always)] #[inline(always)]
pub(crate) fn op_load_builtins<'gc, M: Machine<'gc>>(m: &mut M) -> Step { pub(crate) fn op_load_builtins<'gc, M: Machine<'gc>>(m: &mut M) -> Step {
m.push(m.builtins()); m.push(m.builtins().into());
Step::Continue(()) Step::Continue(())
} }
#[inline(always)] #[inline(always)]
#[expect(
clippy::panic,
reason = "codegen only emits LoadBuiltin with valid BuiltinId bytes; an unknown id is a codegen/bytecode-corruption bug"
)]
pub(crate) fn op_load_builtin<'gc, M: Machine<'gc>>( pub(crate) fn op_load_builtin<'gc, M: Machine<'gc>>(
m: &mut M, m: &mut M,
reader: &mut BytecodeReader<'_>, reader: &mut BytecodeReader<'_>,
) -> Step { ) -> Step {
let Ok(id) = BuiltinId::try_from(reader.read_u8()) let Ok(id) = BuiltinId::try_from(reader.read_u8())
.map_err(|err| panic!("unknown builtin id: {}", err.number)); .map_err(|err| panic!("unknown builtin id: {}", err.number));
m.push(Value::new_inline(PrimOp { m.push(Value::new(PrimOp {
id, id,
arity: BUILTINS[id as usize].1, arity: id.info().arity,
dispatch_ip: Continuation::entry_for_builtin(id).ip(), dispatch_ip: Continuation::entry_for_builtin(id).ip(),
})); }));
Step::Continue(()) Step::Continue(())
@@ -49,7 +53,7 @@ pub(crate) fn op_load_scoped_binding<'gc, M: Machine<'gc>>(
let slot_id = reader.read_u32(); let slot_id = reader.read_u32();
let name = reader.read_string_id(); let name = reader.read_string_id();
let scope = m.scope_slot(slot_id); let scope = m.scope_slot(slot_id);
let Some(attrs) = scope.as_gc::<AttrSet>() else { let Some(attrs) = scope.downcast::<AttrSet>() else {
return m.finish_err(Error::eval_error("internal: scope slot is not an attrset")); return m.finish_err(Error::eval_error("internal: scope slot is not an attrset"));
}; };
match attrs.lookup(name) { match attrs.lookup(name) {
@@ -73,10 +77,10 @@ pub(crate) fn op_coerce_to_string<'gc, M: Machine<'gc>>(
let val = m.force_and_retry::<StrictValue>(reader, mc)?; let val = m.force_and_retry::<StrictValue>(reader, mc)?;
if val.is::<StringId>() || val.is::<NixString>() { if val.is::<StringId>() || val.is::<NixString>() {
m.push(val.relax()); m.push(val.relax());
} else if let Some(p) = val.as_inline::<Path>() { } else if let Some(p) = val.downcast::<Path>() {
// Coercing a path to a string yields the canonical path text. // Coercing a path to a string yields the canonical path text.
// FIXME: copy to store // FIXME: copy to store
m.push(Value::new_inline(p.0)); m.push(Value::new(p.0));
} else { } else {
todo!("coerce other types to string: {:?}", val.ty()); todo!("coerce other types to string: {:?}", val.ty());
} }
@@ -122,10 +126,10 @@ pub(crate) fn op_concat_strings<'gc, M: Machine<'gc>>(
if merged.is_empty() { if merged.is_empty() {
let sid = ctx.intern_string(result); let sid = ctx.intern_string(result);
m.push(Value::new_inline(sid)); m.push(Value::new(sid));
} else { } else {
let ns = gc_arena::Gc::new(mc, NixString::with_context(result, merged)); let ns = gc_arena::Gc::new(mc, NixString::with_context(result, merged));
m.push(Value::new_gc(ns)); m.push(Value::new(ns));
} }
Step::Continue(()) Step::Continue(())
} }
@@ -140,8 +144,8 @@ pub(crate) fn op_resolve_path<'gc, M: MachineExt<'gc>>(
let path_val = m.force_and_retry::<StrictValue>(reader, mc)?; let path_val = m.force_and_retry::<StrictValue>(reader, mc)?;
let dir_id = reader.read_string_id(); let dir_id = reader.read_string_id();
// Already a path: keep as-is. ResolvePath is idempotent on paths. // Already a path: keep as-is. ResolvePath is idempotent on paths.
if let Some(p) = path_val.as_inline::<Path>() { if let Some(p) = path_val.downcast::<Path>() {
m.push(Value::new_inline(p)); m.push(Value::new(p));
return Step::Continue(()); return Step::Continue(());
} }
let path = match ctx.get_string(path_val) { let path = match ctx.get_string(path_val) {
@@ -158,7 +162,7 @@ pub(crate) fn op_resolve_path<'gc, M: MachineExt<'gc>>(
Err(e) => return m.finish_err(e), Err(e) => return m.finish_err(e),
}; };
let sid = ctx.intern_string(resolved); let sid = ctx.intern_string(resolved);
m.push(Value::new_inline(Path(sid))); m.push(Value::new(Path(sid)));
Step::Continue(()) Step::Continue(())
} }
+12
View File
@@ -3,6 +3,10 @@ use fix_runtime::Machine;
use crate::{BytecodeReader, Mutation, Step, Value}; use crate::{BytecodeReader, Mutation, Step, Value};
#[inline(always)] #[inline(always)]
#[expect(
clippy::indexing_slicing,
reason = "local slot index is produced by codegen and bounded by the frame's AllocLocals count"
)]
pub(crate) fn op_load_local<'gc, M: Machine<'gc>>( pub(crate) fn op_load_local<'gc, M: Machine<'gc>>(
m: &mut M, m: &mut M,
reader: &mut BytecodeReader<'_>, reader: &mut BytecodeReader<'_>,
@@ -13,6 +17,10 @@ pub(crate) fn op_load_local<'gc, M: Machine<'gc>>(
} }
#[inline(always)] #[inline(always)]
#[expect(
clippy::indexing_slicing,
reason = "local slot index is produced by codegen and bounded by the target frame's AllocLocals count"
)]
pub(crate) fn op_load_outer<'gc, M: Machine<'gc>>( pub(crate) fn op_load_outer<'gc, M: Machine<'gc>>(
m: &mut M, m: &mut M,
reader: &mut BytecodeReader<'_>, reader: &mut BytecodeReader<'_>,
@@ -30,6 +38,10 @@ pub(crate) fn op_load_outer<'gc, M: Machine<'gc>>(
} }
#[inline(always)] #[inline(always)]
#[expect(
clippy::indexing_slicing,
reason = "local slot index is produced by codegen and bounded by the frame's AllocLocals count"
)]
pub(crate) fn op_store_local<'gc, M: Machine<'gc>>( pub(crate) fn op_store_local<'gc, M: Machine<'gc>>(
m: &mut M, m: &mut M,
reader: &mut BytecodeReader<'_>, reader: &mut BytecodeReader<'_>,
+13 -4
View File
@@ -6,14 +6,21 @@ use smallvec::SmallVec;
use crate::{Break, BytecodeReader, CallFrame, Step, VmRuntimeCtx}; use crate::{Break, BytecodeReader, CallFrame, Step, VmRuntimeCtx};
#[inline(always)] #[inline(always)]
#[expect(
clippy::indexing_slicing,
clippy::cast_sign_loss,
reason = "counter is a non-negative with-scope index in 0..n, staying within the namespaces vec of length n"
)]
pub(crate) fn op_lookup_with<'gc, M: Machine<'gc>>( pub(crate) fn op_lookup_with<'gc, M: Machine<'gc>>(
m: &mut M, m: &mut M,
ctx: &mut impl VmRuntimeCtx, ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>, reader: &mut BytecodeReader<'_>,
mc: &gc_arena::Mutation<'gc>, mc: &gc_arena::Mutation<'gc>,
) -> Step { ) -> Step {
#[allow(clippy::unwrap_used)] let counter = m
let counter = m.peek_forced(0).as_inline::<i32>().unwrap(); .peek_forced(0)
.downcast::<i32>()
.expect("stack slot must be an integer");
let name = reader.read_string_id(); let name = reader.read_string_id();
let n = reader.read_u8(); let n = reader.read_u8();
@@ -34,6 +41,7 @@ pub(crate) fn op_lookup_with<'gc, M: Machine<'gc>>(
thunk: Some(thunk), thunk: Some(thunk),
pc: resume_pc, pc: resume_pc,
env: m.env(), env: m.env(),
depth: None,
}); });
m.set_env(env); m.set_env(env);
reader.set_pc(ip); reader.set_pc(ip);
@@ -45,6 +53,7 @@ pub(crate) fn op_lookup_with<'gc, M: Machine<'gc>>(
thunk: Some(thunk), thunk: Some(thunk),
pc: resume_pc, pc: resume_pc,
env: m.env(), env: m.env(),
depth: None,
}); });
m.push(func); m.push(func);
return m.call(reader, mc, arg, resume_pc); return m.call(reader, mc, arg, resume_pc);
@@ -57,7 +66,7 @@ pub(crate) fn op_lookup_with<'gc, M: Machine<'gc>>(
}; };
if let Some(val) = namespace if let Some(val) = namespace
.as_gc::<AttrSet>() .downcast::<AttrSet>()
.and_then(|attrs| attrs.lookup(name)) .and_then(|attrs| attrs.lookup(name))
{ {
m.replace(0, val); m.replace(0, val);
@@ -67,7 +76,7 @@ pub(crate) fn op_lookup_with<'gc, M: Machine<'gc>>(
Symbol::from(ctx.resolve_string(name)) Symbol::from(ctx.resolve_string(name))
))); )));
} else { } else {
m.replace(0, Value::new_inline(counter + 1)); m.replace(0, Value::new(counter + 1));
reader.set_pc(resume_pc); reader.set_pc(resume_pc);
} }
+42 -34
View File
@@ -1,5 +1,7 @@
#![warn(clippy::unwrap_used)] #![cfg_attr(
#![cfg_attr(feature = "tailcall", expect(incomplete_features))] feature = "tailcall",
expect(incomplete_features, reason = "for testing purpose only")
)]
#![cfg_attr( #![cfg_attr(
feature = "tailcall", feature = "tailcall",
feature(explicit_tail_calls, rust_preserve_none_cc) feature(explicit_tail_calls, rust_preserve_none_cc)
@@ -7,9 +9,9 @@
use std::path::PathBuf; use std::path::PathBuf;
use fix_bytecode::{InstructionPtr, Continuation}; use fix_bytecode::{Continuation, InstructionPtr};
use fix_error::{Error, Result, Source}; use fix_error::{Error, Result, Source};
use fix_lang::{BUILTINS, BuiltinId, StringId}; use fix_lang::{BuiltinId, StringId};
use gc_arena::metrics::Pacing; use gc_arena::metrics::Pacing;
use gc_arena::{Arena, Collect, Gc, Mutation, RefLock, Rootable}; use gc_arena::{Arena, Collect, Gc, Mutation, RefLock, Rootable};
use hashbrown::HashMap; use hashbrown::HashMap;
@@ -18,8 +20,12 @@ use smallvec::SmallVec;
#[cfg(feature = "tailcall")] #[cfg(feature = "tailcall")]
mod dispatch_tailcall; mod dispatch_tailcall;
pub use fix_runtime::*; pub use fix_runtime::*;
#[doc(hidden)]
#[path = "macro_support.rs"]
pub mod __macro_support;
mod instructions; mod instructions;
mod primops; mod primops;
extern crate self as fix_vm;
type VmResult<T> = std::result::Result<T, VmError>; type VmResult<T> = std::result::Result<T, VmError>;
@@ -29,7 +35,10 @@ pub struct Vm<'gc> {
stack: Vec<Value<'gc>>, stack: Vec<Value<'gc>>,
call_stack: Vec<CallFrame<'gc>>, call_stack: Vec<CallFrame<'gc>>,
call_depth: usize, call_depth: usize,
#[allow(dead_code)] #[expect(
dead_code,
reason = "error_context is reserved for tryEval catch-frame tracking, not yet wired up"
)]
#[collect(require_static)] #[collect(require_static)]
error_context: Vec<ErrorFrame>, error_context: Vec<ErrorFrame>,
@@ -38,7 +47,7 @@ pub struct Vm<'gc> {
import_cache: HashMap<PathBuf, Value<'gc>>, import_cache: HashMap<PathBuf, Value<'gc>>,
scope_slots: Vec<Value<'gc>>, scope_slots: Vec<Value<'gc>>,
builtins: Value<'gc>, builtins: Gc<'gc, AttrSet<'gc>>,
empty_list: Value<'gc>, empty_list: Value<'gc>,
empty_attrs: Value<'gc>, empty_attrs: Value<'gc>,
@@ -53,17 +62,16 @@ pub struct Vm<'gc> {
functor_sym: StringId, functor_sym: StringId,
} }
fn init_builtins<'gc>(mc: &Mutation<'gc>, ctx: &mut impl VmRuntimeCtx) -> Value<'gc> { fn init_builtins<'gc>(mc: &Mutation<'gc>, ctx: &mut impl VmRuntimeCtx) -> Gc<'gc, AttrSet<'gc>> {
let mut entries = SmallVec::with_capacity(BUILTINS.len()); let mut entries = SmallVec::with_capacity(BuiltinId::TOTAL);
for (idx, &(name, arity)) in BUILTINS.iter().enumerate() { for id in BuiltinId::ALL {
let id = BuiltinId::try_from(idx as u8).expect("infallible"); let arity = id.info().arity;
let name = name.strip_prefix("__").unwrap_or(name); let name = ctx.intern_string(id.info().name);
let name = ctx.intern_string(name);
let dispatch_ip = Continuation::entry_for_builtin(id).ip(); let dispatch_ip = Continuation::entry_for_builtin(id).ip();
entries.push(( entries.push((
name, name,
Value::new_inline(PrimOp { Value::new(PrimOp {
id, id,
arity, arity,
dispatch_ip, dispatch_ip,
@@ -74,21 +82,15 @@ fn init_builtins<'gc>(mc: &Mutation<'gc>, ctx: &mut impl VmRuntimeCtx) -> Value<
let consts = [ let consts = [
( (
"__currentSystem", "__currentSystem",
Value::new_inline(ctx.intern_string("x86_64-linux")), Value::new(ctx.intern_string("x86_64-linux")),
), ),
("__langVersion", Value::new_inline(6i32)), ("__langVersion", Value::new(6i32)),
( ("__nixVersion", Value::new(ctx.intern_string("2.24.0"))),
"__nixVersion", ("__storeDir", Value::new(ctx.intern_string("/nix/store"))),
Value::new_inline(ctx.intern_string("2.24.0")), ("__nixPath", Value::new(Gc::new(mc, List::default()))),
), ("null", Value::new(Null)),
( ("true", Value::new(true)),
"__storeDir", ("false", Value::new(false)),
Value::new_inline(ctx.intern_string("/nix/store")),
),
("__nixPath", Value::new_gc(Gc::new(mc, List::default()))),
("null", Value::new_inline(Null)),
("true", Value::new_inline(true)),
("false", Value::new_inline(false)),
]; ];
for (name, val) in consts { for (name, val) in consts {
@@ -99,15 +101,15 @@ fn init_builtins<'gc>(mc: &Mutation<'gc>, ctx: &mut impl VmRuntimeCtx) -> Value<
let self_ref_thunk = Gc::new(mc, RefLock::new(ThunkState::Blackhole)); let self_ref_thunk = Gc::new(mc, RefLock::new(ThunkState::Blackhole));
let sym = ctx.intern_string("builtins"); let sym = ctx.intern_string("builtins");
entries.push((sym, Value::new_gc(self_ref_thunk))); entries.push((sym, Value::new(self_ref_thunk)));
entries.sort_by_key(|(k, _)| *k); entries.sort_by_key(|(k, _)| *k);
let builtins_set = Gc::new(mc, AttrSet::from_sorted_unchecked(entries)); let builtins_set = Gc::new(mc, AttrSet::from_sorted_unchecked(entries));
let builtins_value = Value::new_gc(builtins_set); let builtins_value = Value::new(builtins_set);
*self_ref_thunk.borrow_mut(mc) = *self_ref_thunk.borrow_mut(mc) =
ThunkState::Evaluated(builtins_value.restrict().expect("builtins is not a thunk")); ThunkState::Evaluated(builtins_value.restrict().expect("builtins is not a thunk"));
builtins_value builtins_set
} }
impl<'gc> Vm<'gc> { impl<'gc> Vm<'gc> {
@@ -125,8 +127,8 @@ impl<'gc> Vm<'gc> {
scope_slots: Vec::new(), scope_slots: Vec::new(),
builtins, builtins,
empty_list: Value::new_gc(Gc::new(mc, List::default())), empty_list: Value::new(Gc::new(mc, List::default())),
empty_attrs: Value::new_gc(Gc::new(mc, AttrSet::default())), empty_attrs: Value::new(Gc::new(mc, AttrSet::default())),
force_mode, force_mode,
@@ -202,7 +204,7 @@ impl<'gc> Machine<'gc> for Vm<'gc> {
mc: &Mutation<'gc>, mc: &Mutation<'gc>,
resume_pc: usize, resume_pc: usize,
) -> Step { ) -> Step {
let Some(thunk) = self.peek(depth).as_gc::<Thunk>() else { let Some(thunk) = self.peek(depth).downcast::<Thunk>() else {
return Step::Continue(()); return Step::Continue(());
}; };
let mut state = thunk.borrow_mut(mc); let mut state = thunk.borrow_mut(mc);
@@ -213,6 +215,7 @@ impl<'gc> Machine<'gc> for Vm<'gc> {
thunk: Some(thunk), thunk: Some(thunk),
pc: resume_pc, pc: resume_pc,
env: self.env, env: self.env,
depth: Some(depth),
}); });
self.env = env; self.env = env;
reader.set_pc(ip); reader.set_pc(ip);
@@ -227,6 +230,7 @@ impl<'gc> Machine<'gc> for Vm<'gc> {
thunk: Some(thunk), thunk: Some(thunk),
pc: resume_pc, pc: resume_pc,
env: self.env, env: self.env,
depth: Some(depth),
}); });
self.push(func); self.push(func);
self.call(reader, mc, arg, resume_pc) self.call(reader, mc, arg, resume_pc)
@@ -304,7 +308,7 @@ impl<'gc> Machine<'gc> for Vm<'gc> {
} }
#[inline(always)] #[inline(always)]
fn builtins(&self) -> Value<'gc> { fn builtins(&self) -> Gc<'gc, AttrSet<'gc>> {
self.builtins self.builtins
} }
@@ -462,6 +466,10 @@ impl<'gc> Vm<'gc> {
#[inline(always)] #[inline(always)]
#[cfg(not(feature = "tailcall"))] #[cfg(not(feature = "tailcall"))]
#[expect(
clippy::unreachable,
reason = "Op::Illegal is a sentinel never emitted by codegen; reaching it is a codegen bug"
)]
fn execute_batch( fn execute_batch(
&mut self, &mut self,
bytecode: &[u8], bytecode: &[u8],
+36
View File
@@ -0,0 +1,36 @@
//! Single macro-facing path for the `#[primop]` support surface: the traits
//! generated code type-checks through (defined in `fix_runtime`, where the
//! impl sets are coherent) and the stub trio that keeps un-expanded primop
//! sources resolving in tooling.
use std::future::Future;
pub use fix_runtime::{CalleeReady, ForceTarget, UnwrapSlot};
use fix_runtime::{Slot, Value};
macro_rules! type_hint {
() => {
if false {
return async {
unreachable!();
};
}
};
}
#[expect(clippy::panic, reason = "deliberately panic when the stub is called")]
pub fn force<T>(_: Slot<Value<'_>>) -> impl Future<Output = Slot<T>> {
type_hint!();
panic!("stub `force` called at runtime")
}
#[expect(clippy::panic, reason = "deliberately panic when the stub is called")]
pub fn call<T, A, R>(_: &T, _: A) -> impl Future<Output = R> {
type_hint!();
panic!("stub `call` called at runtime")
}
#[expect(clippy::panic, reason = "deliberately panic when the stub is called")]
pub fn spill<T>(_: T) -> Slot<T> {
panic!("stub `spill` called at runtime")
}
+123 -71
View File
@@ -22,11 +22,11 @@ pub fn has_context<'gc, M: Machine<'gc>>(
mc: &Mutation<'gc>, mc: &Mutation<'gc>,
) -> Step { ) -> Step {
let val = m.force_and_retry::<StrictValue>(reader, mc)?; let val = m.force_and_retry::<StrictValue>(reader, mc)?;
if !val.is::<StringId>() && val.as_gc::<NixString>().is_none() { if !val.is::<StringId>() && val.downcast::<NixString>().is_none() {
return m.finish_type_err(NixType::String, val.ty()); return m.finish_type_err(NixType::String, val.ty());
} }
let has_ctx = !ctx.get_string_context(val).is_empty(); let has_ctx = !ctx.get_string_context(val).is_empty();
m.return_from_primop(Value::new_inline(has_ctx), reader) m.return_from_primop(Value::new(has_ctx), reader)
} }
pub fn unsafe_discard_string_context<'gc, M: Machine<'gc>>( pub fn unsafe_discard_string_context<'gc, M: Machine<'gc>>(
@@ -36,14 +36,14 @@ pub fn unsafe_discard_string_context<'gc, M: Machine<'gc>>(
mc: &Mutation<'gc>, mc: &Mutation<'gc>,
) -> Step { ) -> Step {
let val = m.force_and_retry::<StrictValue>(reader, mc)?; let val = m.force_and_retry::<StrictValue>(reader, mc)?;
if let Some(sid) = val.as_inline::<StringId>() { if let Some(sid) = val.downcast::<StringId>() {
return m.return_from_primop(Value::new_inline(sid), reader); return m.return_from_primop(Value::new(sid), reader);
} }
let Some(ns) = val.as_gc::<NixString>() else { let Some(ns) = val.downcast::<NixString>() else {
return m.finish_type_err(NixType::String, val.ty()); return m.finish_type_err(NixType::String, val.ty());
}; };
let sid = ctx.intern_string(ns.as_str()); let sid = ctx.intern_string(ns.as_str());
m.return_from_primop(Value::new_inline(sid), reader) m.return_from_primop(Value::new(sid), reader)
} }
pub fn unsafe_discard_output_dependency<'gc, M: Machine<'gc>>( pub fn unsafe_discard_output_dependency<'gc, M: Machine<'gc>>(
@@ -53,15 +53,15 @@ pub fn unsafe_discard_output_dependency<'gc, M: Machine<'gc>>(
mc: &Mutation<'gc>, mc: &Mutation<'gc>,
) -> Step { ) -> Step {
let val = m.force_and_retry::<StrictValue>(reader, mc)?; let val = m.force_and_retry::<StrictValue>(reader, mc)?;
if let Some(sid) = val.as_inline::<StringId>() { if let Some(sid) = val.downcast::<StringId>() {
return m.return_from_primop(Value::new_inline(sid), reader); return m.return_from_primop(Value::new(sid), reader);
} }
let Some(ns) = val.as_gc::<NixString>() else { let Some(ns) = val.downcast::<NixString>() else {
return m.finish_type_err(NixType::String, val.ty()); return m.finish_type_err(NixType::String, val.ty());
}; };
if ns.context().is_empty() { if ns.context().is_empty() {
let sid = ctx.intern_string(ns.as_str()); let sid = ctx.intern_string(ns.as_str());
return m.return_from_primop(Value::new_inline(sid), reader); return m.return_from_primop(Value::new(sid), reader);
} }
let mut new_ctx = StringContext::new(); let mut new_ctx = StringContext::new();
@@ -77,7 +77,7 @@ pub fn unsafe_discard_output_dependency<'gc, M: Machine<'gc>>(
let s: Box<str> = ns.as_str().into(); let s: Box<str> = ns.as_str().into();
let new_ns = Gc::new(mc, NixString::with_context(s, new_ctx)); let new_ns = Gc::new(mc, NixString::with_context(s, new_ctx));
m.return_from_primop(Value::new_gc(new_ns), reader) m.return_from_primop(Value::new(new_ns), reader)
} }
pub fn get_context<'gc, M: Machine<'gc>>( pub fn get_context<'gc, M: Machine<'gc>>(
@@ -87,7 +87,7 @@ pub fn get_context<'gc, M: Machine<'gc>>(
mc: &Mutation<'gc>, mc: &Mutation<'gc>,
) -> Step { ) -> Step {
let val = m.force_and_retry::<StrictValue>(reader, mc)?; let val = m.force_and_retry::<StrictValue>(reader, mc)?;
if !val.is::<StringId>() && val.as_gc::<NixString>().is_none() { if !val.is::<StringId>() && val.downcast::<NixString>().is_none() {
return m.finish_type_err(NixType::String, val.ty()); return m.finish_type_err(NixType::String, val.ty());
} }
let elems = ctx.get_string_context(val); let elems = ctx.get_string_context(val);
@@ -135,29 +135,29 @@ pub fn get_context<'gc, M: Machine<'gc>>(
let mut sub: SmallVec<[(StringId, Value<'gc>); 4]> = SmallVec::new(); let mut sub: SmallVec<[(StringId, Value<'gc>); 4]> = SmallVec::new();
if info.all_outputs { if info.all_outputs {
sub.push((ctx.intern_string("allOutputs"), Value::new_inline(true))); sub.push((ctx.intern_string("allOutputs"), Value::new(true)));
} }
if !info.outputs.is_empty() { if !info.outputs.is_empty() {
let items: smallvec::SmallVec<[Value<'gc>; 4]> = info let items: smallvec::SmallVec<[Value<'gc>; 4]> = info
.outputs .outputs
.iter() .iter()
.map(|o| Value::new_inline(ctx.intern_string(o))) .map(|o| Value::new(ctx.intern_string(o)))
.collect(); .collect();
let list = VmList::new(mc, items); let list = VmList::new(mc, items);
sub.push((ctx.intern_string("outputs"), Value::new_gc(list))); sub.push((ctx.intern_string("outputs"), Value::new(list)));
} }
if info.path { if info.path {
sub.push((ctx.intern_string("path"), Value::new_inline(true))); sub.push((ctx.intern_string("path"), Value::new(true)));
} }
sub.sort_by_key(|(k, _)| *k); sub.sort_by_key(|(k, _)| *k);
let sub_attrs = Gc::new(mc, AttrSet::from_sorted_unchecked(sub)); let sub_attrs = Gc::new(mc, AttrSet::from_sorted_unchecked(sub));
outer_entries.push((ctx.intern_string(&path), Value::new_gc(sub_attrs))); outer_entries.push((ctx.intern_string(&path), Value::new(sub_attrs)));
} }
outer_entries.sort_by_key(|(k, _)| *k); outer_entries.sort_by_key(|(k, _)| *k);
let outer = Gc::new(mc, AttrSet::from_sorted_unchecked(outer_entries)); let outer = Gc::new(mc, AttrSet::from_sorted_unchecked(outer_entries));
m.return_from_primop(Value::new_gc(outer), reader) m.return_from_primop(Value::new(outer), reader)
} }
/// appendContext :: String -> AttrSet -> String /// appendContext :: String -> AttrSet -> String
@@ -192,24 +192,33 @@ pub fn append_context<'gc, M: Machine<'gc>>(
let acc = Gc::new(mc, NixString::with_context("", initial_ctx)); let acc = Gc::new(mc, NixString::with_context("", initial_ctx));
m.push(str_val.relax()); m.push(str_val.relax());
m.push(Value::new_gc(attrs)); m.push(Value::new(attrs));
m.push(Value::new_inline(0i32)); m.push(Value::new(0i32));
m.push(Value::new_gc(acc)); m.push(Value::new(acc));
reader.set_pc(Continuation::PAppendContextLoop.ip() as usize); reader.set_pc(Continuation::PAppendContextLoop.ip() as usize);
Step::Continue(()) Step::Continue(())
} }
#[expect(
clippy::indexing_slicing,
clippy::cast_sign_loss,
reason = "idx is a non-negative loop counter guarded by `idx as usize >= attrs.entries.len()`, so it indexes attrs.entries in bounds"
)]
pub fn append_context_loop<'gc, M: Machine<'gc>>( pub fn append_context_loop<'gc, M: Machine<'gc>>(
m: &mut M, m: &mut M,
ctx: &mut impl VmRuntimeCtx, ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>, reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>, mc: &Mutation<'gc>,
) -> Step { ) -> Step {
#[allow(clippy::unwrap_used)] let idx = m
let idx = m.peek(1).as_inline::<i32>().unwrap(); .peek(1)
#[allow(clippy::unwrap_used)] .downcast::<i32>()
let attrs = m.peek_forced(2).as_gc::<AttrSet>().unwrap(); .expect("stack slot must be an integer");
let attrs = m
.peek_forced(2)
.downcast::<AttrSet>()
.expect("stack slot must be an attrset");
if idx as usize >= attrs.entries.len() { if idx as usize >= attrs.entries.len() {
return append_context_finalize(m, ctx, reader, mc); return append_context_finalize(m, ctx, reader, mc);
@@ -227,6 +236,11 @@ pub fn append_context_loop<'gc, M: Machine<'gc>>(
Step::Continue(()) Step::Continue(())
} }
#[expect(
clippy::indexing_slicing,
clippy::cast_sign_loss,
reason = "idx is the same non-negative outer-loop counter, in range for outer.entries validated by append_context_loop"
)]
pub fn append_context_entry_forced<'gc, M: Machine<'gc>>( pub fn append_context_entry_forced<'gc, M: Machine<'gc>>(
m: &mut M, m: &mut M,
ctx: &mut impl VmRuntimeCtx, ctx: &mut impl VmRuntimeCtx,
@@ -238,14 +252,18 @@ pub fn append_context_entry_forced<'gc, M: Machine<'gc>>(
// Evaluated value into the slot. // Evaluated value into the slot.
m.force_slot(0, reader, mc)?; m.force_slot(0, reader, mc)?;
let entry_val = m.peek_forced(0); let entry_val = m.peek_forced(0);
let Some(entry_attrs) = entry_val.as_gc::<AttrSet>() else { let Some(entry_attrs) = entry_val.downcast::<AttrSet>() else {
return m.finish_type_err(NixType::AttrSet, entry_val.ty()); return m.finish_type_err(NixType::AttrSet, entry_val.ty());
}; };
#[allow(clippy::unwrap_used)] let idx = m
let idx = m.peek(2).as_inline::<i32>().unwrap(); .peek(2)
#[allow(clippy::unwrap_used)] .downcast::<i32>()
let outer = m.peek_forced(3).as_gc::<AttrSet>().unwrap(); .expect("stack slot must be an integer");
let outer = m
.peek_forced(3)
.downcast::<AttrSet>()
.expect("stack slot must be an attrset");
let path_key = outer.entries[idx as usize].0; let path_key = outer.entries[idx as usize].0;
let path_str_owned: Box<str> = ctx.resolve_string(path_key).into(); let path_str_owned: Box<str> = ctx.resolve_string(path_key).into();
if !path_str_owned.starts_with("/nix/store/") { if !path_str_owned.starts_with("/nix/store/") {
@@ -262,12 +280,14 @@ pub fn append_context_entry_forced<'gc, M: Machine<'gc>>(
let all_outputs_id = ctx.intern_string("allOutputs"); let all_outputs_id = ctx.intern_string("allOutputs");
let outputs_id = ctx.intern_string("outputs"); let outputs_id = ctx.intern_string("outputs");
#[allow(clippy::unwrap_used)] let acc_gc = m
let acc_gc = m.peek(1).as_gc::<NixString>().unwrap(); .peek(1)
.downcast::<NixString>()
.expect("stack slot must be a string");
let mut new_acc: StringContext = acc_gc.context().iter().cloned().collect(); let mut new_acc: StringContext = acc_gc.context().iter().cloned().collect();
if let Some(v) = entry_attrs.lookup(path_id) if let Some(v) = entry_attrs.lookup(path_id)
&& v.as_inline::<bool>() == Some(true) && v.downcast::<bool>() == Some(true)
{ {
new_acc.insert(StringContextElem::Opaque { new_acc.insert(StringContextElem::Opaque {
path: path_str_owned.clone(), path: path_str_owned.clone(),
@@ -275,7 +295,7 @@ pub fn append_context_entry_forced<'gc, M: Machine<'gc>>(
} }
if let Some(v) = entry_attrs.lookup(all_outputs_id) if let Some(v) = entry_attrs.lookup(all_outputs_id)
&& v.as_inline::<bool>() == Some(true) && v.downcast::<bool>() == Some(true)
{ {
if !path_str_owned.ends_with(".drv") { if !path_str_owned.ends_with(".drv") {
return m.finish_err(Error::eval_error(format!( return m.finish_err(Error::eval_error(format!(
@@ -288,7 +308,7 @@ pub fn append_context_entry_forced<'gc, M: Machine<'gc>>(
} }
let new_acc_gc = Gc::new(mc, NixString::with_context("", new_acc)); let new_acc_gc = Gc::new(mc, NixString::with_context("", new_acc));
m.replace(1, Value::new_gc(new_acc_gc)); m.replace(1, Value::new(new_acc_gc));
if let Some(outputs_val) = entry_attrs.lookup(outputs_id) { if let Some(outputs_val) = entry_attrs.lookup(outputs_id) {
m.replace(0, outputs_val); m.replace(0, outputs_val);
@@ -302,10 +322,12 @@ pub fn append_context_entry_forced<'gc, M: Machine<'gc>>(
return Step::Continue(()); return Step::Continue(());
} }
let _ = m.pop(); m.drop_n(1);
#[allow(clippy::unwrap_used)] let idx_back = m
let idx_back = m.peek(1).as_inline::<i32>().unwrap(); .peek(1)
m.replace(1, Value::new_inline(idx_back + 1)); .downcast::<i32>()
.expect("stack slot must be an integer");
m.replace(1, Value::new(idx_back + 1));
reader.set_pc(Continuation::PAppendContextLoop.ip() as usize); reader.set_pc(Continuation::PAppendContextLoop.ip() as usize);
Step::Continue(()) Step::Continue(())
} }
@@ -318,43 +340,55 @@ pub fn append_context_outputs_forced<'gc, M: Machine<'gc>>(
) -> Step { ) -> Step {
m.force_slot(0, reader, mc)?; m.force_slot(0, reader, mc)?;
let list_val = m.peek_forced(0); let list_val = m.peek_forced(0);
let Some(list) = list_val.as_gc::<VmList>() else { let Some(list) = list_val.downcast::<VmList>() else {
return m.finish_type_err(NixType::List, list_val.ty()); return m.finish_type_err(NixType::List, list_val.ty());
}; };
if list.inner.borrow().is_empty() { if list.inner.borrow().is_empty() {
// Stack: [strVal, attrs, idx, acc, list] -> drop list, bump idx. // Stack: [strVal, attrs, idx, acc, list] -> drop list, bump idx.
let _ = m.pop(); m.drop_n(1);
#[allow(clippy::unwrap_used)] let idx_back = m
let idx_back = m.peek(1).as_inline::<i32>().unwrap(); .peek(1)
m.replace(1, Value::new_inline(idx_back + 1)); .downcast::<i32>()
.expect("stack slot must be an integer");
m.replace(1, Value::new(idx_back + 1));
reader.set_pc(Continuation::PAppendContextLoop.ip() as usize); reader.set_pc(Continuation::PAppendContextLoop.ip() as usize);
return Step::Continue(()); return Step::Continue(());
} }
m.push(Value::new_inline(0i32)); m.push(Value::new(0i32));
reader.set_pc(Continuation::PAppendContextOutputElementLoop.ip() as usize); reader.set_pc(Continuation::PAppendContextOutputElementLoop.ip() as usize);
Step::Continue(()) Step::Continue(())
} }
#[expect(
clippy::indexing_slicing,
clippy::cast_sign_loss,
reason = "oidx is a non-negative loop counter guarded by `oidx as usize >= len`, so it indexes the list in bounds"
)]
pub fn append_context_output_element_loop<'gc, M: Machine<'gc>>( pub fn append_context_output_element_loop<'gc, M: Machine<'gc>>(
m: &mut M, m: &mut M,
_ctx: &mut impl VmRuntimeCtx, _ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>, reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>, mc: &Mutation<'gc>,
) -> Step { ) -> Step {
#[allow(clippy::unwrap_used)] let oidx = m
let oidx = m.peek(0).as_inline::<i32>().unwrap(); .peek(0)
#[allow(clippy::unwrap_used)] .downcast::<i32>()
let list = m.peek_forced(1).as_gc::<VmList>().unwrap(); .expect("stack slot must be an integer");
let list = m
.peek_forced(1)
.downcast::<VmList>()
.expect("stack slot must be a list");
let len = list.inner.borrow().len(); let len = list.inner.borrow().len();
if oidx as usize >= len { if oidx as usize >= len {
// Stack: [strVal, attrs, idx, acc, list, oidx] -> drop oidx & list, // Stack: [strVal, attrs, idx, acc, list, oidx] -> drop oidx & list,
// bump idx in place. // bump idx in place.
let _ = m.pop(); m.drop_n(2);
let _ = m.pop(); let idx_back = m
#[allow(clippy::unwrap_used)] .peek(1)
let idx_back = m.peek(1).as_inline::<i32>().unwrap(); .downcast::<i32>()
m.replace(1, Value::new_inline(idx_back + 1)); .expect("stack slot must be an integer");
m.replace(1, Value::new(idx_back + 1));
reader.set_pc(Continuation::PAppendContextLoop.ip() as usize); reader.set_pc(Continuation::PAppendContextLoop.ip() as usize);
return Step::Continue(()); return Step::Continue(());
} }
@@ -371,6 +405,11 @@ pub fn append_context_output_element_loop<'gc, M: Machine<'gc>>(
Step::Continue(()) Step::Continue(())
} }
#[expect(
clippy::indexing_slicing,
clippy::cast_sign_loss,
reason = "idx is the non-negative outer-loop counter, in range for outer.entries validated by append_context_loop"
)]
pub fn append_context_output_element_forced<'gc, M: Machine<'gc>>( pub fn append_context_output_element_forced<'gc, M: Machine<'gc>>(
m: &mut M, m: &mut M,
ctx: &mut impl VmRuntimeCtx, ctx: &mut impl VmRuntimeCtx,
@@ -384,10 +423,14 @@ pub fn append_context_output_element_forced<'gc, M: Machine<'gc>>(
}; };
let output_name: Box<str> = output_name.into(); let output_name: Box<str> = output_name.into();
#[allow(clippy::unwrap_used)] let idx = m
let idx = m.peek(4).as_inline::<i32>().unwrap(); .peek(4)
#[allow(clippy::unwrap_used)] .downcast::<i32>()
let outer = m.peek_forced(5).as_gc::<AttrSet>().unwrap(); .expect("stack slot must be an integer");
let outer = m
.peek_forced(5)
.downcast::<AttrSet>()
.expect("stack slot must be an attrset");
let path_key = outer.entries[idx as usize].0; let path_key = outer.entries[idx as usize].0;
let path_str: Box<str> = ctx.resolve_string(path_key).into(); let path_str: Box<str> = ctx.resolve_string(path_key).into();
if !path_str.ends_with(".drv") { if !path_str.ends_with(".drv") {
@@ -396,26 +439,34 @@ pub fn append_context_output_element_forced<'gc, M: Machine<'gc>>(
))); )));
} }
#[allow(clippy::unwrap_used)] let acc_gc = m
let acc_gc = m.peek(3).as_gc::<NixString>().unwrap(); .peek(3)
.downcast::<NixString>()
.expect("stack slot must be a string");
let mut new_acc: StringContext = acc_gc.context().iter().cloned().collect(); let mut new_acc: StringContext = acc_gc.context().iter().cloned().collect();
new_acc.insert(StringContextElem::Built { new_acc.insert(StringContextElem::Built {
drv_path: path_str, drv_path: path_str,
output: output_name, output: output_name,
}); });
let new_acc_gc = Gc::new(mc, NixString::with_context("", new_acc)); let new_acc_gc = Gc::new(mc, NixString::with_context("", new_acc));
m.replace(3, Value::new_gc(new_acc_gc)); m.replace(3, Value::new(new_acc_gc));
// Stack: [strVal, attrs, idx, acc, list, oidx, outElem] -> drop outElem, // Stack: [strVal, attrs, idx, acc, list, oidx, outElem] -> drop outElem,
// bump oidx in place. // bump oidx in place.
let _ = m.pop(); m.drop_n(1);
#[allow(clippy::unwrap_used)] let oidx = m
let oidx = m.peek(0).as_inline::<i32>().unwrap(); .peek(0)
m.replace(0, Value::new_inline(oidx + 1)); .downcast::<i32>()
.expect("stack slot must be an integer");
m.replace(0, Value::new(oidx + 1));
reader.set_pc(Continuation::PAppendContextOutputElementLoop.ip() as usize); reader.set_pc(Continuation::PAppendContextOutputElementLoop.ip() as usize);
Step::Continue(()) Step::Continue(())
} }
#[expect(
clippy::panic,
reason = "strVal was forced to WHNF at appendContext entry, so restrict() can never observe a thunk here"
)]
fn append_context_finalize<'gc, M: Machine<'gc>>( fn append_context_finalize<'gc, M: Machine<'gc>>(
m: &mut M, m: &mut M,
ctx: &mut impl VmRuntimeCtx, ctx: &mut impl VmRuntimeCtx,
@@ -423,10 +474,11 @@ fn append_context_finalize<'gc, M: Machine<'gc>>(
mc: &Mutation<'gc>, mc: &Mutation<'gc>,
) -> Step { ) -> Step {
// Stack: [strVal, attrs, idx, acc] // Stack: [strVal, attrs, idx, acc]
#[allow(clippy::unwrap_used)] let acc_gc = m
let acc_gc = m.pop().as_gc::<NixString>().unwrap(); .pop()
let _ = m.pop(); // idx .downcast::<NixString>()
let _ = m.pop(); // attrs .expect("stack slot must be a string");
m.drop_n(2);
let str_val_raw = m.pop(); let str_val_raw = m.pop();
// The strVal was already forced at entry; restrict() is infallible here. // The strVal was already forced at entry; restrict() is infallible here.
@@ -438,10 +490,10 @@ fn append_context_finalize<'gc, M: Machine<'gc>>(
let context: StringContext = acc_gc.context().iter().cloned().collect(); let context: StringContext = acc_gc.context().iter().cloned().collect();
let result = if context.is_empty() { let result = if context.is_empty() {
let sid = ctx.intern_string(s_str); let sid = ctx.intern_string(s_str);
Value::new_inline(sid) Value::new(sid)
} else { } else {
let ns = Gc::new(mc, NixString::with_context(s_str, context)); let ns = Gc::new(mc, NixString::with_context(s_str, context));
Value::new_gc(ns) Value::new(ns)
}; };
m.return_from_primop(result, reader) m.return_from_primop(result, reader)
} }
+105 -154
View File
@@ -1,22 +1,89 @@
use fix_bytecode::Continuation; use fix_bytecode::Continuation;
use fix_error::Error; use fix_error::{Error, Result};
use fix_macros::handler;
use fix_runtime::{ use fix_runtime::{
AttrSet, BytecodeReader, Closure, Env, List, Machine, MachineExt, Step, StrictValue, Value, AttrSet, BytecodeReader, Closure, Env, List, Machine, MachineExt, Slot, Step, StrictValue,
VmRuntimeCtx, VmRuntimeCtxExt, Value, VmRuntimeCtx, VmRuntimeCtxExt,
}; };
use gc_arena::{Gc, Mutation, RefLock}; use gc_arena::{Gc, Mutation, RefLock};
use smallvec::SmallVec; use smallvec::SmallVec;
pub fn seq<'gc, M: Machine<'gc>>( use crate::primops::stubs::*;
m: &mut M,
reader: &mut BytecodeReader<'_>, #[handler(name = PSeq)]
fn seq<'gc>(mc: &Mutation<'gc>, e1: Slot<Value<'gc>>, e2: Slot<Value<'gc>>) -> Result<Value<'gc>> {
let _e1: Slot<StrictValue<'gc>> = force(e1).await?;
Ok(e2.get())
}
#[handler(name = PDeepSeq)]
fn deep_seq<'gc>(
mc: &Mutation<'gc>, mc: &Mutation<'gc>,
) -> Step { e1: Slot<Value<'gc>>,
// stack: [e1, e2] - force e1, return e2 e2: Slot<Value<'gc>>,
m.force_slot(1, reader, mc)?; ) -> Result<Value<'gc>> {
let e2 = m.pop(); let e1: Slot<StrictValue<'gc>> = force(e1).await?;
let _ = m.pop(); if collect_children(e1.get()).is_empty() {
m.return_from_primop(e2, reader) return Ok(e2.get());
}
let seen: Slot<List<'gc>> = spill(Gc::new(mc, List::default()));
let worklist: Slot<List<'gc>> = spill(List::new(mc, collect_children(e1.get())));
let count: Slot<i32> = spill(worklist.get().inner.borrow().len() as i32);
loop {
if count.get() == 0 {
return Ok(e2.get());
}
let item = worklist
.get()
.unlock(mc)
.borrow_mut()
.pop()
.expect("worklist is non-empty while count > 0");
count.set(count.get() - 1);
let item: Slot<StrictValue<'gc>> = force(item).await?;
let added = push_children(mc, seen.get(), worklist.get(), item);
count.set(count.get() + added);
}
}
fn collect_children<'gc>(val: StrictValue<'gc>) -> SmallVec<[Value<'gc>; 4]> {
if let Some(attrs) = val.downcast::<AttrSet<'gc>>() {
attrs.entries.iter().map(|&(_, v)| v).collect()
} else if let Some(list) = val.downcast::<List<'gc>>() {
list.inner.borrow().iter().copied().collect()
} else {
SmallVec::new()
}
}
fn push_children<'gc>(
mc: &Mutation<'gc>,
seen: Gc<'gc, List<'gc>>,
worklist: Gc<'gc, List<'gc>>,
item: StrictValue<'gc>,
) -> i32 {
let mut added = 0i32;
if let Some(attrs) = item.downcast::<AttrSet<'gc>>()
&& !is_value_in_seen(seen, item.relax())
{
add_value_to_seen(seen, mc, item.relax());
let mut wl = worklist.unlock(mc).borrow_mut();
for &(_, v) in attrs.entries.iter() {
wl.push(v);
}
added = attrs.entries.len() as i32;
} else if let Some(list) = item.downcast::<List<'gc>>()
&& !is_value_in_seen(seen, item.relax())
{
add_value_to_seen(seen, mc, item.relax());
let inner = list.inner.borrow();
let mut wl = worklist.unlock(mc).borrow_mut();
for &v in inner.iter() {
wl.push(v);
}
added = inner.len() as i32;
}
added
} }
pub fn abort<'gc, M: Machine<'gc>>( pub fn abort<'gc, M: Machine<'gc>>(
@@ -34,133 +101,6 @@ pub fn abort<'gc, M: Machine<'gc>>(
))) )))
} }
pub fn deep_seq_force_top<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
// stack: [e1, e2] - force e1, return e2
m.force_slot(1, reader, mc)?;
let e1 = m.peek_forced(1);
let children: SmallVec<_> = if let Some(attrs) = e1.as_gc::<AttrSet>() {
let attrs = &attrs.entries;
if attrs.is_empty() {
SmallVec::new()
} else {
attrs.iter().map(|&(_, v)| v).collect()
}
} else if let Some(list) = e1.as_gc::<List<'gc>>() {
let inner = list.inner.borrow();
if inner.is_empty() {
SmallVec::new()
} else {
inner.iter().copied().collect()
}
} else {
SmallVec::new()
};
if children.is_empty() {
let e2 = m.pop();
let _ = m.pop();
return m.return_from_primop(e2, reader);
}
let count = children.len() as i32;
let seen: Gc<'gc, List<'gc>> = Gc::new(mc, List::default());
let worklist: Gc<'gc, List<'gc>> = List::new(mc, children);
let e2 = m.pop();
let _ = m.pop();
m.push(e2);
m.push(Value::new_gc(seen));
m.push(Value::new_gc(worklist));
m.push(Value::new_inline(count));
reader.set_pc(Continuation::PDeepSeqPush.ip() as usize);
Step::Continue(())
}
pub fn deep_seq_push<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
// stack: [e2, seen, worklist, counter]
#[allow(clippy::unwrap_used)]
let counter = m.peek(0).as_inline::<i32>().unwrap();
if counter == 0 {
let _ = m.pop(); // counter
let _ = m.pop(); // worklist
let _ = m.pop(); // seen
let val = m.pop();
return m.return_from_primop(val, reader);
}
#[allow(clippy::unwrap_used)]
let worklist = m.peek_forced(1).as_gc::<List<'gc>>().unwrap();
#[allow(clippy::unwrap_used)]
let item = worklist.unlock(mc).borrow_mut().pop().unwrap();
m.replace(0, Value::new_inline(counter - 1));
m.push(item);
// force item at TOS, resume at DeepSeqLoop after force
m.force_slot_to_pc(0, reader, mc, Continuation::PDeepSeqLoop.ip() as usize)?;
reader.set_pc(Continuation::PDeepSeqLoop.ip() as usize);
Step::Continue(())
}
pub fn deep_seq_loop<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
// stack after pop: [e2, seen, worklist, counter]
let item = m.pop();
#[allow(clippy::unwrap_used)]
let counter = m.peek(0).as_inline::<i32>().unwrap();
let mut added: usize = 0;
if let Some(attrs) = item.as_gc::<AttrSet>() {
let attrs = &attrs.entries;
#[allow(clippy::unwrap_used)]
let seen = m.peek_forced(2).as_gc::<List<'gc>>().unwrap();
if !is_value_in_seen(seen, item) {
add_value_to_seen(seen, mc, item);
#[allow(clippy::unwrap_used)]
let worklist = m.peek_forced(1).as_gc::<List<'gc>>().unwrap();
{
let mut wl = worklist.unlock(mc).borrow_mut();
for &(_, v) in attrs.iter() {
wl.push(v);
}
added = attrs.len();
}
}
} else if let Some(list) = item.as_gc::<List<'gc>>() {
#[allow(clippy::unwrap_used)]
let seen = m.peek_forced(2).as_gc::<List<'gc>>().unwrap();
if !is_value_in_seen(seen, item) {
add_value_to_seen(seen, mc, item);
#[allow(clippy::unwrap_used)]
let worklist = m.peek_forced(1).as_gc::<List<'gc>>().unwrap();
{
let inner = list.inner.borrow();
let mut wl = worklist.unlock(mc).borrow_mut();
for &v in inner.iter() {
wl.push(v);
}
added = inner.len();
}
}
}
m.replace(0, Value::new_inline(counter + added as i32));
reader.set_pc(Continuation::PDeepSeqPush.ip() as usize);
Step::Continue(())
}
pub fn force_result_shallow<'gc, M: Machine<'gc>>( pub fn force_result_shallow<'gc, M: Machine<'gc>>(
m: &mut M, m: &mut M,
ctx: &mut impl VmRuntimeCtx, ctx: &mut impl VmRuntimeCtx,
@@ -170,10 +110,10 @@ pub fn force_result_shallow<'gc, M: Machine<'gc>>(
m.force_slot(0, reader, mc)?; m.force_slot(0, reader, mc)?;
let val = m.peek_forced(0); let val = m.peek_forced(0);
let (count, has_children) = if let Some(attrs) = val.as_gc::<AttrSet>() { let (count, has_children) = if let Some(attrs) = val.downcast::<AttrSet>() {
let len = attrs.entries.len(); let len = attrs.entries.len();
(len, len > 0) (len, len > 0)
} else if let Some(list) = val.as_gc::<List<'gc>>() { } else if let Some(list) = val.downcast::<List>() {
let len = list.inner.borrow().len(); let len = list.inner.borrow().len();
(len, len > 0) (len, len > 0)
} else { } else {
@@ -185,41 +125,48 @@ pub fn force_result_shallow<'gc, M: Machine<'gc>>(
return m.finish_ok(ctx.convert_value(val)); return m.finish_ok(ctx.convert_value(val));
} }
m.push(Value::new_inline(0i32)); m.push(Value::new(0i32));
m.push(Value::new_inline(count as i32)); m.push(Value::new(count as i32));
reader.set_pc(Continuation::ForceResultShallowPush.ip() as usize); reader.set_pc(Continuation::ForceResultShallowPush.ip() as usize);
Step::Continue(()) Step::Continue(())
} }
#[expect(
clippy::cast_sign_loss,
reason = "idx is a non-negative loop counter (0..=len); used only with .get(), so the cast is always a valid index or safely out of range"
)]
pub fn force_result_shallow_push<'gc, M: Machine<'gc>>( pub fn force_result_shallow_push<'gc, M: Machine<'gc>>(
m: &mut M, m: &mut M,
ctx: &mut impl VmRuntimeCtx, ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>, reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>, mc: &Mutation<'gc>,
) -> Step { ) -> Step {
#[allow(clippy::unwrap_used)] let idx = m
let idx = m.peek(1).as_inline::<i32>().unwrap(); .peek(1)
#[allow(clippy::unwrap_used)] .downcast::<i32>()
let len = m.peek(0).as_inline::<i32>().unwrap(); .expect("stack slot must be an integer");
let len = m
.peek(0)
.downcast::<i32>()
.expect("stack slot must be an integer");
if idx == len { if idx == len {
let _ = m.pop(); // len m.drop_n(2);
let _ = m.pop(); // idx
let val = m.pop(); let val = m.pop();
return m.finish_ok(ctx.convert_value(val)); return m.finish_ok(ctx.convert_value(val));
} }
let val = m.peek_forced(2); let val = m.peek_forced(2);
let child = if let Some(attrs) = val.as_gc::<AttrSet>() { let child = if let Some(attrs) = val.downcast::<AttrSet>() {
attrs.entries.get(idx as usize).map(|&(_, v)| v) attrs.entries.get(idx as usize).map(|&(_, v)| v)
} else if let Some(list) = val.as_gc::<List<'gc>>() { } else if let Some(list) = val.downcast::<List>() {
list.inner.borrow().get(idx as usize).copied() list.inner.borrow().get(idx as usize).copied()
} else { } else {
None None
}; };
if let Some(child) = child { if let Some(child) = child {
m.replace(1, Value::new_inline(idx + 1)); m.replace(1, Value::new(idx + 1));
m.push(child); m.push(child);
m.force_slot_to_pc( m.force_slot_to_pc(
0, 0,
@@ -237,7 +184,7 @@ pub fn force_result_shallow_loop<'gc, M: Machine<'gc>>(
reader: &mut BytecodeReader<'_>, reader: &mut BytecodeReader<'_>,
_mc: &Mutation<'gc>, _mc: &Mutation<'gc>,
) -> Step { ) -> Step {
let _ = m.pop(); // forced child m.drop_n(1);
reader.set_pc(Continuation::ForceResultShallowPush.ip() as usize); reader.set_pc(Continuation::ForceResultShallowPush.ip() as usize);
Step::Continue(()) Step::Continue(())
} }
@@ -308,6 +255,10 @@ pub fn call_functor_2<'gc, M: Machine<'gc>>(
m.call(reader, mc, orig_arg, saved.pc) m.call(reader, mc, orig_arg, saved.pc)
} }
#[expect(
clippy::unreachable,
reason = "CallPattern is only dispatched for closures whose pattern is Some, established when the Call opcode routed here"
)]
pub fn call_pattern<'gc, M: Machine<'gc>>( pub fn call_pattern<'gc, M: Machine<'gc>>(
m: &mut M, m: &mut M,
ctx: &mut impl VmRuntimeCtx, ctx: &mut impl VmRuntimeCtx,
@@ -349,7 +300,7 @@ pub fn call_pattern<'gc, M: Machine<'gc>>(
let new_env = Gc::new( let new_env = Gc::new(
mc, mc,
RefLock::new(Env::with_arg(Value::new_gc(attrset), n_locals, env)), RefLock::new(Env::with_arg(Value::new(attrset), n_locals, env)),
); );
reader.set_pc(ip as usize); reader.set_pc(ip as usize);
m.set_env(new_env); m.set_env(new_env);
+7 -3
View File
@@ -16,8 +16,8 @@ pub fn to_string<'gc, M: Machine<'gc>>(
if val.is::<StringId>() || val.is::<NixString>() { if val.is::<StringId>() || val.is::<NixString>() {
return m.return_from_primop(val.relax(), reader); return m.return_from_primop(val.relax(), reader);
} }
if let Some(p) = val.as_inline::<Path>() { if let Some(p) = val.downcast::<Path>() {
return m.return_from_primop(Value::new_inline(p.0), reader); return m.return_from_primop(Value::new(p.0), reader);
} }
// TODO: derivations / `__toString` / `outPath`, // TODO: derivations / `__toString` / `outPath`,
// numbers, lists. // numbers, lists.
@@ -27,6 +27,10 @@ pub fn to_string<'gc, M: Machine<'gc>>(
))) )))
} }
#[expect(
clippy::unreachable,
reason = "val was forced to WHNF by force_and_retry, so its type is never Thunk here"
)]
pub fn type_of<'gc, M: Machine<'gc>>( pub fn type_of<'gc, M: Machine<'gc>>(
m: &mut M, m: &mut M,
ctx: &mut impl VmRuntimeCtx, ctx: &mut impl VmRuntimeCtx,
@@ -47,5 +51,5 @@ pub fn type_of<'gc, M: Machine<'gc>>(
NixType::Thunk => unreachable!("forced"), NixType::Thunk => unreachable!("forced"),
}; };
let sid = ctx.intern_string(name); let sid = ctx.intern_string(name);
m.return_from_primop(Value::new_inline(sid), reader) m.return_from_primop(Value::new(sid), reader)
} }
+25 -22
View File
@@ -17,11 +17,11 @@ pub fn start_eq<'gc, M: Machine<'gc>>(
) -> Step { ) -> Step {
match shallow_eq(ctx, lhs, rhs) { match shallow_eq(ctx, lhs, rhs) {
ShallowEq::True => { ShallowEq::True => {
m.push(Value::new_inline(!negate)); m.push(Value::new(!negate));
Step::Continue(()) Step::Continue(())
} }
ShallowEq::False => { ShallowEq::False => {
m.push(Value::new_inline(negate)); m.push(Value::new(negate));
Step::Continue(()) Step::Continue(())
} }
ShallowEq::RecurseList(la, lb) => { ShallowEq::RecurseList(la, lb) => {
@@ -44,15 +44,15 @@ pub fn eq_step<'gc, M: Machine<'gc>>(
) -> Step { ) -> Step {
let rhs_q = m let rhs_q = m
.peek(0) .peek(0)
.as_gc::<List<'gc>>() .downcast::<List>()
.expect("eq state corrupted: rhs_queue"); .expect("eq state corrupted: rhs_queue");
let lhs_q = m let lhs_q = m
.peek(1) .peek(1)
.as_gc::<List<'gc>>() .downcast::<List>()
.expect("eq state corrupted: lhs_queue"); .expect("eq state corrupted: lhs_queue");
let result = m let result = m
.peek(2) .peek(2)
.as_inline::<bool>() .downcast::<bool>()
.expect("eq state corrupted: result"); .expect("eq state corrupted: result");
if !result || lhs_q.inner.borrow().is_empty() { if !result || lhs_q.inner.borrow().is_empty() {
@@ -88,17 +88,16 @@ pub fn eq_force<'gc, M: Machine<'gc>>(
} }
fn finalize<'gc, M: Machine<'gc>>(m: &mut M, reader: &mut BytecodeReader<'_>) -> Step { fn finalize<'gc, M: Machine<'gc>>(m: &mut M, reader: &mut BytecodeReader<'_>) -> Step {
let _ = m.pop(); m.drop_n(2);
let _ = m.pop();
let result = m let result = m
.pop() .pop()
.as_inline::<bool>() .downcast::<bool>()
.expect("eq state corrupted: result"); .expect("eq state corrupted: result");
let negate = m let negate = m
.pop() .pop()
.as_inline::<bool>() .downcast::<bool>()
.expect("eq state corrupted: negate"); .expect("eq state corrupted: negate");
m.return_from_primop(Value::new_inline(result ^ negate), reader) m.return_from_primop(Value::new(result ^ negate), reader)
} }
fn apply_pair<'gc, M: Machine<'gc>>( fn apply_pair<'gc, M: Machine<'gc>>(
@@ -111,7 +110,7 @@ fn apply_pair<'gc, M: Machine<'gc>>(
match shallow_eq(ctx, lhs, rhs) { match shallow_eq(ctx, lhs, rhs) {
ShallowEq::True => {} ShallowEq::True => {}
ShallowEq::False => { ShallowEq::False => {
m.replace(2, Value::new_inline(false)); m.replace(2, Value::new(false));
} }
ShallowEq::RecurseList(la, lb) => { ShallowEq::RecurseList(la, lb) => {
extend_queues( extend_queues(
@@ -140,11 +139,11 @@ where
{ {
let rhs_q = m let rhs_q = m
.peek(0) .peek(0)
.as_gc::<List<'gc>>() .downcast::<List>()
.expect("eq state corrupted: rhs_queue"); .expect("eq state corrupted: rhs_queue");
let lhs_q = m let lhs_q = m
.peek(1) .peek(1)
.as_gc::<List<'gc>>() .downcast::<List>()
.expect("eq state corrupted: lhs_queue"); .expect("eq state corrupted: lhs_queue");
let mut lq = lhs_q.unlock(mc).borrow_mut(); let mut lq = lhs_q.unlock(mc).borrow_mut();
let mut rq = rhs_q.unlock(mc).borrow_mut(); let mut rq = rhs_q.unlock(mc).borrow_mut();
@@ -167,12 +166,13 @@ fn enter_eq_machine<'gc, M: Machine<'gc>>(
pc: resume_pc, pc: resume_pc,
thunk: None, thunk: None,
env: m.env(), env: m.env(),
depth: None,
}); });
m.inc_call_depth(); m.inc_call_depth();
m.push(Value::new_inline(negate)); m.push(Value::new(negate));
m.push(Value::new_inline(true)); m.push(Value::new(true));
m.push(Value::new_gc(List::new(mc, lhs_init))); m.push(Value::new(List::new(mc, lhs_init)));
m.push(Value::new_gc(List::new(mc, rhs_init))); m.push(Value::new(List::new(mc, rhs_init)));
reader.set_pc(Continuation::EqStep.ip() as usize); reader.set_pc(Continuation::EqStep.ip() as usize);
Step::Continue(()) Step::Continue(())
} }
@@ -189,7 +189,7 @@ fn shallow_eq<'gc>(
lhs: StrictValue<'gc>, lhs: StrictValue<'gc>,
rhs: StrictValue<'gc>, rhs: StrictValue<'gc>,
) -> ShallowEq<'gc> { ) -> ShallowEq<'gc> {
if let (Some(a), Some(b)) = (lhs.as_num(), rhs.as_num()) { if let (Some(a), Some(b)) = (lhs.downcast_num(), rhs.downcast_num()) {
let eq = match (a, b) { let eq = match (a, b) {
(NixNum::Int(a), NixNum::Int(b)) => a == b, (NixNum::Int(a), NixNum::Int(b)) => a == b,
(NixNum::Float(a), NixNum::Float(b)) => a == b, (NixNum::Float(a), NixNum::Float(b)) => a == b,
@@ -198,25 +198,28 @@ fn shallow_eq<'gc>(
}; };
return bool_outcome(eq); return bool_outcome(eq);
} }
if let (Some(a), Some(b)) = (lhs.as_inline::<bool>(), rhs.as_inline::<bool>()) { if let (Some(a), Some(b)) = (lhs.downcast::<bool>(), rhs.downcast::<bool>()) {
return bool_outcome(a == b); return bool_outcome(a == b);
} }
if lhs.is::<Null>() && rhs.is::<Null>() { if lhs.is::<Null>() && rhs.is::<Null>() {
return ShallowEq::True; return ShallowEq::True;
} }
if let (Some(a), Some(b)) = (lhs.as_inline::<Path>(), rhs.as_inline::<Path>()) { if let (Some(a), Some(b)) = (lhs.downcast::<Path>(), rhs.downcast::<Path>()) {
return bool_outcome(a.0 == b.0); return bool_outcome(a.0 == b.0);
} }
if let (Some(a), Some(b)) = (ctx.get_string(lhs), ctx.get_string(rhs)) { if let (Some(a), Some(b)) = (ctx.get_string(lhs), ctx.get_string(rhs)) {
return bool_outcome(a == b); return bool_outcome(a == b);
} }
if let (Some(a), Some(b)) = (lhs.as_gc::<List<'gc>>(), rhs.as_gc::<List<'gc>>()) { if let (Some(a), Some(b)) = (lhs.downcast::<List>(), rhs.downcast::<List>()) {
if a.inner.borrow().len() != b.inner.borrow().len() { if a.inner.borrow().len() != b.inner.borrow().len() {
return ShallowEq::False; return ShallowEq::False;
} }
return ShallowEq::RecurseList(a, b); return ShallowEq::RecurseList(a, b);
} }
if let (Some(a), Some(b)) = (lhs.as_gc::<AttrSet<'gc>>(), rhs.as_gc::<AttrSet<'gc>>()) { if let (Some(a), Some(b)) = (
lhs.downcast::<AttrSet<'gc>>(),
rhs.downcast::<AttrSet<'gc>>(),
) {
let ae = &a.entries; let ae = &a.entries;
let be = &b.entries; let be = &b.entries;
if ae.len() != be.len() { if ae.len() != be.len() {
+15 -6
View File
@@ -40,12 +40,13 @@ pub fn import<'gc, M: Machine<'gc>>(
// finalizer can use it as the cache key. The slot we pop here was // finalizer can use it as the cache key. The slot we pop here was
// freed by `force_and_retry`, so we simply push. // freed by `force_and_retry`, so we simply push.
let path_sid = ctx.intern_string(abs.to_string_lossy()); let path_sid = ctx.intern_string(abs.to_string_lossy());
m.push(Value::new_inline(path_sid)); m.push(Value::new(path_sid));
let env = m.env(); let env = m.env();
m.push_call_frame(CallFrame { m.push_call_frame(CallFrame {
pc: Continuation::PImportFinalize.ip() as usize, pc: Continuation::PImportFinalize.ip() as usize,
thunk: None, thunk: None,
env, env,
depth: None,
}); });
m.set_pending_load(PendingLoad { m.set_pending_load(PendingLoad {
@@ -55,6 +56,10 @@ pub fn import<'gc, M: Machine<'gc>>(
Step::Break(Break::LoadFile) Step::Break(Break::LoadFile)
} }
#[expect(
clippy::unreachable,
reason = "import always pushes the PImportFinalize call frame before this runs, so pop_call_frame is always Some"
)]
pub fn import_finalize<'gc, M: Machine<'gc>>( pub fn import_finalize<'gc, M: Machine<'gc>>(
m: &mut M, m: &mut M,
ctx: &mut impl VmRuntimeCtx, ctx: &mut impl VmRuntimeCtx,
@@ -62,8 +67,10 @@ pub fn import_finalize<'gc, M: Machine<'gc>>(
) -> Step { ) -> Step {
// stack: [path_sid, return_value] // stack: [path_sid, return_value]
let val = m.pop(); let val = m.pop();
#[allow(clippy::unwrap_used)] let path_sid = m
let path_sid = m.pop().as_inline::<StringId>().unwrap(); .pop()
.downcast::<StringId>()
.expect("stack slot must be a string");
// The cache key is keyed by the absolute path string we interned in // The cache key is keyed by the absolute path string we interned in
// `import`. Resolve it back to the host PathBuf. // `import`. Resolve it back to the host PathBuf.
let path_str = ctx.resolve_string(path_sid).to_owned(); let path_str = ctx.resolve_string(path_sid).to_owned();
@@ -73,6 +80,7 @@ pub fn import_finalize<'gc, M: Machine<'gc>>(
pc: ret_pc, pc: ret_pc,
thunk: _, thunk: _,
env, env,
depth: None,
}) = m.pop_call_frame() }) = m.pop_call_frame()
else { else {
unreachable!() unreachable!()
@@ -107,13 +115,14 @@ pub fn scoped_import<'gc, M: Machine<'gc>>(
}; };
let keys: HashSet<StringId> = scope_attrs.entries.iter().map(|&(k, _)| k).collect(); let keys: HashSet<StringId> = scope_attrs.entries.iter().map(|&(k, _)| k).collect();
let slot_id = m.scope_slots_push(Value::new_gc(scope_attrs)); let slot_id = m.scope_slots_push(Value::new(scope_attrs));
let env = m.env(); let env = m.env();
m.push_call_frame(CallFrame { m.push_call_frame(CallFrame {
pc: Continuation::PScopedImportFinalize.ip() as usize, pc: Continuation::PScopedImportFinalize.ip() as usize,
thunk: None, thunk: None,
env, env,
depth: None,
}); });
m.set_pending_load(PendingLoad { m.set_pending_load(PendingLoad {
@@ -146,7 +155,7 @@ pub fn path_exists<'gc, M: Machine<'gc>>(
let path_val = m.force_and_retry::<StrictValue>(reader, mc)?; let path_val = m.force_and_retry::<StrictValue>(reader, mc)?;
// pathExists requires an absolute path. A `Path` value is // pathExists requires an absolute path. A `Path` value is
// always absolute; a string is accepted only if it starts with `/`. // always absolute; a string is accepted only if it starts with `/`.
let (path, is_path_value) = if let Some(p) = path_val.as_inline::<Path>() { let (path, is_path_value) = if let Some(p) = path_val.downcast::<Path>() {
(ctx.resolve_string(p.0).to_owned(), true) (ctx.resolve_string(p.0).to_owned(), true)
} else if let Some(s) = ctx.get_string(path_val) { } else if let Some(s) = ctx.get_string(path_val) {
(s.to_owned(), false) (s.to_owned(), false)
@@ -171,7 +180,7 @@ pub fn path_exists<'gc, M: Machine<'gc>>(
} else { } else {
std::fs::symlink_metadata(p).is_ok() std::fs::symlink_metadata(p).is_ok()
}; };
m.return_from_primop(Value::new_inline(exists), reader) m.return_from_primop(Value::new(exists), reader)
} }
/// Convert the user-supplied path string into an absolute, dotted-segment /// Convert the user-supplied path string into an absolute, dotted-segment
+98 -265
View File
@@ -1,283 +1,116 @@
use fix_bytecode::Continuation; use fix_error::Result;
use fix_runtime::{BytecodeReader, List, Machine, MachineExt, NixType, Step, StrictValue, Value}; use fix_macros::handler;
use fix_runtime::{List, Slot, StrictValue, Value};
use gc_arena::Mutation; use gc_arena::Mutation;
pub fn filter_force_list<'gc, M: Machine<'gc>>( use crate::primops::stubs::*;
m: &mut M,
reader: &mut BytecodeReader<'_>, #[handler(name = PFilter)]
fn filter<'gc>(
mc: &Mutation<'gc>, mc: &Mutation<'gc>,
) -> Step { pred: Slot<Value<'gc>>,
m.force_slot(0, reader, mc)?; list: Slot<Value<'gc>>,
let list = match m.peek_forced(0).expect_gc::<List>() { ) -> Result<Value<'gc>> {
Ok(list) => list, let list: Slot<List<'gc>> = force(list).await?;
Err(got) => return m.finish_type_err(NixType::List, got), if list.get().inner.borrow().is_empty() {
}; return Ok(Value::new(list.get()));
if list.inner.borrow().is_empty() {
let val = m.pop();
let _pred = m.pop();
return m.return_from_primop(val, reader);
} }
// prepare stack layout: [ pred list idx acc ] let idx: Slot<i32> = spill(0i32);
m.push(Value::new_inline(0)); let acc: Slot<List<'gc>> = spill(List::new_gc(mc));
m.push(Value::new_gc(List::new_gc(mc))); loop {
reader.set_pc(Continuation::PFilterCallPred.ip() as usize); #[expect(
Step::Continue(()) clippy::indexing_slicing,
} reason = "idx is a non-negative loop counter in 0..list.len(), so it indexes the list in bounds"
)]
pub fn filter_call_pred<'gc, M: Machine<'gc>>( let keep: bool = call(&pred, list.get().inner.borrow()[idx.get() as usize]).await?;
m: &mut M, if keep {
reader: &mut BytecodeReader<'_>, #[expect(
mc: &Mutation<'gc>, clippy::indexing_slicing,
) -> Step { reason = "idx is a non-negative loop counter in 0..list.len(), so it indexes the list in bounds"
m.force_slot(3, reader, mc)?; )]
let pred = m.peek_forced(3); let elem = list.get().inner.borrow()[idx.get() as usize];
#[allow(clippy::unwrap_used)] acc.get().unlock(mc).borrow_mut().push(elem);
let idx = m.peek(1).as_inline::<i32>().unwrap(); }
#[allow(clippy::unwrap_used)] if idx.get() as usize == list.get().inner.borrow().len() - 1 {
let elem = m.peek_forced(2).as_gc::<List>().unwrap().inner.borrow()[idx as usize]; return Ok(Value::new(acc.get()));
m.push(pred.relax()); }
m.call(reader, mc, elem, Continuation::PFilterCheck.ip() as usize) idx.set(idx.get() + 1);
}
pub fn filter_check<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let ret = m.force_and_retry::<bool>(reader, mc)?;
#[allow(clippy::unwrap_used)]
let idx = m.peek(1).as_inline::<i32>().unwrap();
#[allow(clippy::unwrap_used)]
let list = m.peek_forced(2).as_gc::<List>().unwrap();
let list = list.inner.borrow();
#[allow(clippy::unwrap_used)]
let acc = m.peek_forced(0).as_gc::<List>().unwrap();
if ret {
let mut acc = acc.unlock(mc).borrow_mut();
acc.push(list[idx as usize]);
} }
if idx as usize == list.len() - 1 { }
let acc = m.pop();
let _ = m.pop(); // idx #[handler(name = PAll)]
let _ = m.pop(); // list fn all<'gc>(
let _ = m.pop(); // pred mc: &Mutation<'gc>,
return m.return_from_primop(acc, reader); pred: Slot<Value<'gc>>,
list: Slot<Value<'gc>>,
) -> Result<Value<'gc>> {
let list: Slot<List<'gc>> = force(list).await?;
if list.get().inner.borrow().is_empty() {
return Ok(Value::new(true));
} }
m.replace(1, Value::new_inline(idx + 1)); let idx: Slot<i32> = spill(0i32);
reader.set_pc(Continuation::PFilterCallPred.ip() as usize); loop {
Step::Continue(()) #[expect(
} clippy::indexing_slicing,
reason = "idx is a non-negative loop counter in 0..list.len(), so it indexes the list in bounds"
// foldl' op nul list )]
// let keep: bool = call(&pred, list.get().inner.borrow()[idx.get() as usize]).await?;
// Stack layouts across phases: if !keep || idx.get() as usize + 1 == list.get().inner.borrow().len() {
// Entry: [op, nul, list] return Ok(Value::new(keep));
// Empty: [op, nul] }
// Call1: [op, list, idx, acc] idx.set(idx.get() + 1);
// Call2: [op, list, idx, acc, intermediate]
// Update: [op, list, idx, acc, result]
pub fn foldl_strict_entry<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
m.force_slot(0, reader, mc)?;
let list_val = m.peek_forced(0);
let Some(list) = list_val.as_gc::<List>() else {
return m.finish_type_err(NixType::List, list_val.ty());
};
if list.inner.borrow().is_empty() {
let _ = m.pop(); // list
reader.set_pc(Continuation::PFoldlStrictEmpty.ip() as usize);
return Step::Continue(());
} }
let list_val = m.pop();
let nul_val = m.pop();
m.push(list_val);
m.push(Value::new_inline(0i32));
m.push(nul_val);
reader.set_pc(Continuation::PFoldlStrictCall1.ip() as usize);
Step::Continue(())
} }
pub fn foldl_strict_empty<'gc, M: Machine<'gc>>( #[handler(name = PAny)]
m: &mut M, fn any<'gc>(
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>, mc: &Mutation<'gc>,
) -> Step { pred: Slot<Value<'gc>>,
let nul = m.force_and_retry::<StrictValue>(reader, mc)?; list: Slot<Value<'gc>>,
let _ = m.pop(); // op ) -> Result<Value<'gc>> {
m.return_from_primop(nul.relax(), reader) let list: Slot<List<'gc>> = force(list).await?;
} if list.get().inner.borrow().is_empty() {
return Ok(Value::new(false));
pub fn foldl_strict_call1<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
m.force_slot(3, reader, mc)?;
let op = m.peek_forced(3);
let acc = m.peek(0);
m.push(op.relax());
m.call(reader, mc, acc, Continuation::PFoldlStrictCall2.ip() as usize)
}
pub fn foldl_strict_call2<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
#[allow(clippy::unwrap_used)]
let idx = m.peek(2).as_inline::<i32>().unwrap();
#[allow(clippy::unwrap_used)]
let list = m.peek_forced(3).as_gc::<List>().unwrap();
let elem = list.inner.borrow()[idx as usize];
m.call(
reader,
mc,
elem,
Continuation::PFoldlStrictUpdate.ip() as usize,
)
}
pub fn foldl_strict_update<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
_mc: &Mutation<'gc>,
) -> Step {
let result = m.pop();
m.replace(0, result);
#[allow(clippy::unwrap_used)]
let idx = m.peek(1).as_inline::<i32>().unwrap();
#[allow(clippy::unwrap_used)]
let list = m.peek_forced(2).as_gc::<List>().unwrap();
let len = list.inner.borrow().len();
if (idx as usize) + 1 == len {
let acc = m.pop();
let _ = m.pop(); // idx
let _ = m.pop(); // list
let _ = m.pop(); // op
return m.return_from_primop(acc, reader);
} }
m.replace(1, Value::new_inline(idx + 1)); let idx: Slot<i32> = spill(0i32);
reader.set_pc(Continuation::PFoldlStrictCall1.ip() as usize); loop {
Step::Continue(()) #[expect(
} clippy::indexing_slicing,
reason = "idx is a non-negative loop counter in 0..list.len(), so it indexes the list in bounds"
pub fn all_entry<'gc, M: Machine<'gc>>( )]
m: &mut M, let keep: bool = call(&pred, list.get().inner.borrow()[idx.get() as usize]).await?;
reader: &mut BytecodeReader<'_>, if keep || idx.get() as usize + 1 == list.get().inner.borrow().len() {
mc: &Mutation<'gc>, return Ok(Value::new(keep));
) -> Step { }
m.force_slot(0, reader, mc)?; idx.set(idx.get() + 1);
let list = match m.peek_forced(0).expect_gc::<List>() {
Ok(list) => list,
Err(got) => return m.finish_type_err(NixType::List, got),
};
// FIXME: force callable
m.force_slot(1, reader, mc)?;
if list.inner.borrow().is_empty() {
let _list = m.pop();
let _pred = m.pop();
return m.return_from_primop(Value::new_inline(true), reader);
} }
// prepare stack layout: [ pred list idx ]
m.push(Value::new_inline(0));
reader.set_pc(Continuation::PAllCallPred.ip() as usize);
Step::Continue(())
} }
pub fn all_call_pred<'gc, M: Machine<'gc>>( #[handler(name = PFoldlStrict)]
m: &mut M, fn foldl_strict<'gc>(
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>, mc: &Mutation<'gc>,
) -> Step { op: Slot<Value<'gc>>,
let pred = m.peek_forced(2); nul: Slot<Value<'gc>>,
#[allow(clippy::unwrap_used)] list: Slot<Value<'gc>>,
let idx = m.peek(0).as_inline::<i32>().unwrap(); ) -> Result<Value<'gc>> {
#[allow(clippy::unwrap_used)] let list: Slot<List<'gc>> = force(list).await?;
let elem = m.peek_forced(1).as_gc::<List>().unwrap().inner.borrow()[idx as usize]; if list.get().inner.borrow().is_empty() {
m.push(pred.relax()); return Ok(nul.get());
m.call(reader, mc, elem, Continuation::PAllCheck.ip() as usize)
}
pub fn all_check<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let ret = m.force_and_retry::<bool>(reader, mc)?;
#[allow(clippy::unwrap_used)]
let idx = m.peek(0).as_inline::<i32>().unwrap();
#[allow(clippy::unwrap_used)]
let list = m.peek_forced(1).as_gc::<List>().unwrap();
let list = list.inner.borrow();
if idx as usize == list.len() - 1 || !ret {
let _ = m.pop(); // idx
let _ = m.pop(); // list
let _ = m.pop(); // pred
return m.return_from_primop(Value::new_inline(ret), reader);
} }
m.replace(0, Value::new_inline(idx + 1)); let idx: Slot<i32> = spill(0i32);
reader.set_pc(Continuation::PAllCallPred.ip() as usize); let acc = spill(nul.get());
Step::Continue(()) loop {
} let f = call(&op, acc.get()).await?;
#[expect(
pub fn any_entry<'gc, M: Machine<'gc>>( clippy::indexing_slicing,
m: &mut M, reason = "idx is a non-negative loop counter in 0..list.len(), so it indexes the list in bounds"
reader: &mut BytecodeReader<'_>, )]
mc: &Mutation<'gc>, let new_acc: StrictValue<'gc> =
) -> Step { call(&f, list.get().inner.borrow()[idx.get() as usize]).await?;
m.force_slot(0, reader, mc)?; acc.set(new_acc.relax());
let list = match m.peek_forced(0).expect_gc::<List>() { if idx.get() as usize + 1 == list.get().inner.borrow().len() {
Ok(list) => list, return Ok(acc.get());
Err(got) => return m.finish_type_err(NixType::List, got), }
}; idx.set(idx.get() + 1);
// FIXME: force callable
m.force_slot(1, reader, mc)?;
if list.inner.borrow().is_empty() {
let _list = m.pop();
let _pred = m.pop();
return m.return_from_primop(Value::new_inline(false), reader);
} }
// prepare stack layout: [ pred list idx ]
m.push(Value::new_inline(0));
reader.set_pc(Continuation::PAnyCallPred.ip() as usize);
Step::Continue(())
}
pub fn any_call_pred<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let pred = m.peek_forced(2);
#[allow(clippy::unwrap_used)]
let idx = m.peek(0).as_inline::<i32>().unwrap();
#[allow(clippy::unwrap_used)]
let elem = m.peek_forced(1).as_gc::<List>().unwrap().inner.borrow()[idx as usize];
m.push(pred.relax());
m.call(reader, mc, elem, Continuation::PAnyCheck.ip() as usize)
}
pub fn any_check<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let ret = m.force_and_retry::<bool>(reader, mc)?;
#[allow(clippy::unwrap_used)]
let idx = m.peek(0).as_inline::<i32>().unwrap();
#[allow(clippy::unwrap_used)]
let list = m.peek_forced(1).as_gc::<List>().unwrap();
let list = list.inner.borrow();
if idx as usize == list.len() - 1 || ret {
let _ = m.pop(); // idx
let _ = m.pop(); // list
let _ = m.pop(); // pred
return m.return_from_primop(Value::new_inline(ret), reader);
}
m.replace(0, Value::new_inline(idx + 1));
reader.set_pc(Continuation::PAnyCallPred.ip() as usize);
Step::Continue(())
} }
+11 -21
View File
@@ -5,6 +5,7 @@ mod eq;
mod io; mod io;
mod list; mod list;
mod path; mod path;
mod stubs;
pub use context::*; pub use context::*;
pub use control::*; pub use control::*;
@@ -18,7 +19,6 @@ pub use io::*;
pub use list::*; pub use list::*;
pub use path::*; pub use path::*;
#[allow(clippy::too_many_lines)]
pub fn dispatch_cont<'gc, M: Machine<'gc>>( pub fn dispatch_cont<'gc, M: Machine<'gc>>(
m: &mut M, m: &mut M,
ctx: &mut impl VmRuntimeCtx, ctx: &mut impl VmRuntimeCtx,
@@ -33,28 +33,18 @@ pub fn dispatch_cont<'gc, M: Machine<'gc>>(
match cont { match cont {
PAbort => abort(m, ctx, reader, mc), PAbort => abort(m, ctx, reader, mc),
PAll => all_entry(m, reader, mc), PAll0 | PAll1 | PAll2 | PAll3 => all(m, reader, mc, cont),
PAllCallPred => all_call_pred(m, reader, mc), PAny0 | PAny1 | PAny2 | PAny3 => any(m, reader, mc, cont),
PAllCheck => all_check(m, reader, mc), PDeepSeq0 | PDeepSeq1 | PDeepSeq2 | PDeepSeq3 | PDeepSeq4 | PDeepSeq5 => {
deep_seq(m, reader, mc, cont)
}
PSeq0 | PSeq1 => seq(m, reader, mc, cont),
PAny => any_entry(m, reader, mc), PFilter0 | PFilter1 | PFilter2 | PFilter3 | PFilter4 => filter(m, reader, mc, cont),
PAnyCallPred => any_call_pred(m, reader, mc),
PAnyCheck => any_check(m, reader, mc),
PDeepSeq => deep_seq_force_top(m, reader, mc), PFoldlStrict0 | PFoldlStrict1 | PFoldlStrict2 | PFoldlStrict3 | PFoldlStrict4 | PFoldlStrict5 => {
PDeepSeqPush => deep_seq_push(m, reader, mc), foldl_strict(m, reader, mc, cont)
PDeepSeqLoop => deep_seq_loop(m, reader, mc), }
PSeq => seq(m, reader, mc),
PFilterForceList => filter_force_list(m, reader, mc),
PFilterCallPred => filter_call_pred(m, reader, mc),
PFilterCheck => filter_check(m, reader, mc),
PFoldlStrict => foldl_strict_entry(m, reader, mc),
PFoldlStrictEmpty => foldl_strict_empty(m, reader, mc),
PFoldlStrictCall1 => foldl_strict_call1(m, reader, mc),
PFoldlStrictCall2 => foldl_strict_call2(m, reader, mc),
PFoldlStrictUpdate => foldl_strict_update(m, reader, mc),
ForceResultShallow => force_result_shallow(m, ctx, reader, mc), ForceResultShallow => force_result_shallow(m, ctx, reader, mc),
ForceResultShallowPush => force_result_shallow_push(m, ctx, reader, mc), ForceResultShallowPush => force_result_shallow_push(m, ctx, reader, mc),
+4 -4
View File
@@ -13,8 +13,8 @@ pub fn to_path<'gc, M: Machine<'gc>>(
) -> Step { ) -> Step {
// coerce to path THEN TO STRING // coerce to path THEN TO STRING
let val = m.force_and_retry::<StrictValue>(reader, mc)?; let val = m.force_and_retry::<StrictValue>(reader, mc)?;
if let Some(Path(s)) = val.as_inline::<Path>() { if let Some(Path(s)) = val.downcast::<Path>() {
return m.return_from_primop(Value::new_inline(s), reader); return m.return_from_primop(Value::new(s), reader);
} }
let Some(s) = ctx.get_string(val) else { let Some(s) = ctx.get_string(val) else {
return m.finish_err(Error::eval_error(format!( return m.finish_err(Error::eval_error(format!(
@@ -29,7 +29,7 @@ pub fn to_path<'gc, M: Machine<'gc>>(
} }
let canon = canon_path_str(s); let canon = canon_path_str(s);
let sid = ctx.intern_string(canon); let sid = ctx.intern_string(canon);
m.return_from_primop(Value::new_inline(sid), reader) m.return_from_primop(Value::new(sid), reader)
} }
pub fn is_path<'gc, M: Machine<'gc>>( pub fn is_path<'gc, M: Machine<'gc>>(
@@ -39,5 +39,5 @@ pub fn is_path<'gc, M: Machine<'gc>>(
) -> Step { ) -> Step {
let val = m.force_and_retry::<StrictValue>(reader, mc)?; let val = m.force_and_retry::<StrictValue>(reader, mc)?;
let is_path = val.is::<Path>(); let is_path = val.is::<Path>();
m.return_from_primop(Value::new_inline(is_path), reader) m.return_from_primop(Value::new(is_path), reader)
} }
+9
View File
@@ -0,0 +1,9 @@
//! Fallback definitions for the `#[primop]` surface syntax.
//!
//! `force`, `call`, and `spill` are recognized and rewritten by the
//! `#[primop]` proc macro, so compiled code never calls them. They exist only
//! so the un-expanded source name-resolves in tooling that does not run the
//! macro (e.g. rust-analyzer while the macro crate is being rebuilt). Calling
//! one for real is impossible: they never return.
pub use crate::__macro_support::{call, force, spill};
+4
View File
@@ -48,5 +48,9 @@ fix-vm = { path = "../fix-vm" }
[dev-dependencies] [dev-dependencies]
criterion = { version = "0.8", features = ["html_reports"] } criterion = { version = "0.8", features = ["html_reports"] }
serial_test = "4.0"
tempfile = "3.24" tempfile = "3.24"
test-log = { version = "0.2", features = ["trace"] } test-log = { version = "0.2", features = ["trace"] }
[lints]
workspace = true
+2 -1
View File
@@ -1,4 +1,5 @@
#![allow(dead_code)] #![allow(clippy::allow_attributes_without_reason)]
#![allow(dead_code, clippy::unwrap_used, clippy::unwrap_in_result)]
use fix::Evaluator; use fix::Evaluator;
use fix_error::{Result, Source}; use fix_error::{Result, Source};
+7 -11
View File
@@ -1,6 +1,3 @@
#![warn(clippy::unwrap_used)]
#![allow(dead_code)]
use fix_bytecode::InstructionPtr; use fix_bytecode::InstructionPtr;
use fix_bytecode::disassembler::{Disassembler, DisassemblerContext}; use fix_bytecode::disassembler::{Disassembler, DisassemblerContext};
use fix_compiler::{CodeState, ExtraScope}; use fix_compiler::{CodeState, ExtraScope};
@@ -11,7 +8,6 @@ use fix_vm::Vm;
use hashbrown::{HashMap, HashSet}; use hashbrown::{HashMap, HashSet};
use string_interner::{DefaultStringInterner, Symbol as _}; use string_interner::{DefaultStringInterner, Symbol as _};
mod derivation;
pub mod logging; pub mod logging;
#[global_allocator] #[global_allocator]
@@ -101,12 +97,12 @@ impl VmRuntimeCtx for RuntimeState {
StringId(self.strings.get_or_intern(s)) StringId(self.strings.get_or_intern(s))
} }
fn resolve_string(&self, id: StringId) -> &str { fn resolve_string(&self, id: StringId) -> &str {
#[allow(clippy::unwrap_used)] self.strings
self.strings.resolve(id.0).unwrap() .resolve(id.0)
.expect("interned string id must resolve")
} }
fn get_const(&self, id: u32) -> StaticValue { fn get_const(&self, id: u32) -> StaticValue {
#[allow(clippy::unwrap_used)] self.constants.get(id).expect("const id must be valid")
self.constants.get(id).unwrap()
} }
fn add_const(&mut self, val: StaticValue) -> u32 { fn add_const(&mut self, val: StaticValue) -> u32 {
self.constants.insert(val) self.constants.insert(val)
@@ -145,9 +141,9 @@ impl DisassemblerContext for Evaluator {
&self.code.bytecode &self.code.bytecode
} }
#[allow(clippy::unwrap_used)]
fn resolve_string(&self, id: u32) -> &str { fn resolve_string(&self, id: u32) -> &str {
let id = string_interner::symbol::SymbolU32::try_from_usize(id as usize).unwrap(); let id = string_interner::symbol::SymbolU32::try_from_usize(id as usize)
self.runtime.strings.resolve(id).unwrap() .expect("invalid string id");
self.runtime.strings.resolve(id).expect("invalid string id")
} }
} }
+3 -4
View File
@@ -5,7 +5,7 @@ use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt; use tracing_subscriber::util::SubscriberInitExt;
use tracing_subscriber::{EnvFilter, Layer, fmt}; use tracing_subscriber::{EnvFilter, Layer, fmt};
pub fn init_logging() { pub fn init_logging() -> Result<(), miette::InstallError> {
let is_terminal = std::io::stderr().is_terminal(); let is_terminal = std::io::stderr().is_terminal();
let show_time = env::var("NIX_JS_LOG_TIME") let show_time = env::var("NIX_JS_LOG_TIME")
.map(|v| v == "1" || v.to_lowercase() == "true") .map(|v| v == "1" || v.to_lowercase() == "true")
@@ -32,10 +32,10 @@ pub fn init_logging() {
.with(fmt_layer) .with(fmt_layer)
.init(); .init();
init_miette_handler(); init_miette_handler()
} }
fn init_miette_handler() { fn init_miette_handler() -> Result<(), miette::InstallError> {
let is_terminal = std::io::stderr().is_terminal(); let is_terminal = std::io::stderr().is_terminal();
miette::set_hook(Box::new(move |_| { miette::set_hook(Box::new(move |_| {
Box::new( Box::new(
@@ -46,5 +46,4 @@ fn init_miette_handler() {
.build(), .build(),
) )
})) }))
.ok();
} }
+34 -19
View File
@@ -20,33 +20,49 @@ struct Cli {
enum Command { enum Command {
Compile { Compile {
#[clap(flatten)] #[clap(flatten)]
source: ExprSource, source: ExprSourceArgs,
#[arg(long)] #[arg(long)]
silent: bool, silent: bool,
}, },
Eval { Eval {
#[clap(flatten)] #[clap(flatten)]
source: ExprSource, source: ExprSourceArgs,
}, },
Repl, Repl,
} }
#[derive(Args)] #[derive(Args)]
#[group(required = true, multiple = false)] #[group(required = true, multiple = false)]
struct ExprSource { struct ExprSourceArgs {
#[clap(short, long)] #[clap(short, long)]
expr: Option<String>, expr: Option<String>,
#[clap(short, long)] #[clap(short, long)]
file: Option<PathBuf>, file: Option<PathBuf>,
} }
enum ExprSource {
Expr(String),
File(PathBuf),
}
#[expect(
clippy::unreachable,
reason = "clap's arg group guarantees exactly one of --expr/--file is set"
)]
impl From<ExprSourceArgs> for ExprSource {
fn from(args: ExprSourceArgs) -> Self {
match (args.expr, args.file) {
(Some(expr), None) => ExprSource::Expr(expr),
(None, Some(file)) => ExprSource::File(file),
_ => unreachable!(),
}
}
}
fn run_compile(eval: &mut Evaluator, src: ExprSource, silent: bool) -> Result<()> { fn run_compile(eval: &mut Evaluator, src: ExprSource, silent: bool) -> Result<()> {
let src = if let Some(expr) = src.expr { let src = match src {
Source::new_eval(expr)? ExprSource::Expr(expr) => Source::new_eval(expr)?,
} else if let Some(file) = src.file { ExprSource::File(file) => Source::new_file(file)?,
Source::new_file(file)?
} else {
unreachable!()
}; };
match eval.compile_bytecode(src) { match eval.compile_bytecode(src) {
Ok(ip) => { Ok(ip) => {
@@ -63,12 +79,9 @@ fn run_compile(eval: &mut Evaluator, src: ExprSource, silent: bool) -> Result<()
} }
fn run_eval(eval: &mut Evaluator, src: ExprSource) -> Result<()> { fn run_eval(eval: &mut Evaluator, src: ExprSource) -> Result<()> {
let src = if let Some(expr) = src.expr { let src = match src {
Source::new_eval(expr)? ExprSource::Expr(expr) => Source::new_eval(expr)?,
} else if let Some(file) = src.file { ExprSource::File(file) => Source::new_file(file)?,
Source::new_file(file)?
} else {
unreachable!()
}; };
match eval.eval_deep(src) { match eval.eval_deep(src) {
Ok(value) => { Ok(value) => {
@@ -93,7 +106,9 @@ fn run_repl(eval: &mut Evaluator) -> Result<()> {
if line.trim().is_empty() { if line.trim().is_empty() {
continue; continue;
} }
let _ = rl.add_history_entry(line.as_str()); if let Err(err) = rl.add_history_entry(line.as_str()) {
eprintln!("[WARN] Failed to add history entry: {err}");
}
if let Some([Some(_), Some(ident), Some(rest)]) = RE.exec(&line) { if let Some([Some(_), Some(ident), Some(rest)]) = RE.exec(&line) {
if let Some(expr) = rest.strip_prefix('=') { if let Some(expr) = rest.strip_prefix('=') {
let expr = expr.trim_start(); let expr = expr.trim_start();
@@ -137,15 +152,15 @@ fn run_repl(eval: &mut Evaluator) -> Result<()> {
} }
fn main() -> Result<()> { fn main() -> Result<()> {
fix::logging::init_logging(); fix::logging::init_logging()?;
let cli = Cli::parse(); let cli = Cli::parse();
let mut eval = Evaluator::new(); let mut eval = Evaluator::new();
match cli.command { match cli.command {
Command::Compile { source, silent } => run_compile(&mut eval, source, silent), Command::Compile { source, silent } => run_compile(&mut eval, source.into(), silent),
Command::Eval { source } => run_eval(&mut eval, source), Command::Eval { source } => run_eval(&mut eval, source.into()),
Command::Repl => run_repl(&mut eval), Command::Repl => run_repl(&mut eval),
} }
} }
+4 -6
View File
@@ -402,17 +402,15 @@ fn fixed_output_sha256_flat() {
#[test_log::test] #[test_log::test]
fn fixed_output_missing_hashalgo() { fn fixed_output_missing_hashalgo() {
assert!( eval_deep_result(
eval_deep_result( r#"derivation {
r#"derivation {
name = "default"; name = "default";
builder = "/bin/sh"; builder = "/bin/sh";
system = "x86_64-linux"; system = "x86_64-linux";
outputHash = "0000000000000000000000000000000000000000000000000000000000000000"; outputHash = "0000000000000000000000000000000000000000000000000000000000000000";
}"#, }"#,
) )
.is_err() .unwrap_err();
);
} }
#[test_log::test] #[test_log::test]
+1 -1
View File
@@ -350,7 +350,7 @@ fn read_dir_nonexistent_fails() {
let expr = r#"builtins.readDir "/nonexistent/directory""#; let expr = r#"builtins.readDir "/nonexistent/directory""#;
let result = eval_result(expr); let result = eval_result(expr);
assert!(result.is_err()); result.unwrap_err();
} }
#[test_log::test] #[test_log::test]
+18 -7
View File
@@ -5,6 +5,7 @@ use std::path::PathBuf;
use fix::Evaluator; use fix::Evaluator;
use fix_error::{Source, SourceType}; use fix_error::{Source, SourceType};
use fix_lang::Value; use fix_lang::Value;
use serial_test::serial;
fn get_lang_dir() -> PathBuf { fn get_lang_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/tests/lang") PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/tests/lang")
@@ -160,9 +161,14 @@ mod okay {
eval_okay_test!(getattrpos); eval_okay_test!(getattrpos);
eval_okay_test!(getattrpos_functionargs); eval_okay_test!(getattrpos_functionargs);
eval_okay_test!(getattrpos_undefined); eval_okay_test!(getattrpos_undefined);
eval_okay_test!(getenv, || { eval_okay_test!(
unsafe { std::env::set_var("TEST_VAR", "foo") }; #[serial(env)]
}); getenv,
|| {
// SAFETY: guarded with #[serial_test::serial]
unsafe { std::env::set_var("TEST_VAR", "foo") };
}
);
eval_okay_test!(groupBy); eval_okay_test!(groupBy);
eval_okay_test!(r#if); eval_okay_test!(r#if);
eval_okay_test!(ind_string); eval_okay_test!(ind_string);
@@ -194,11 +200,16 @@ mod okay {
eval_okay_test!(partition); eval_okay_test!(partition);
eval_okay_test!(path); eval_okay_test!(path);
eval_okay_test!(pathexists); eval_okay_test!(pathexists);
eval_okay_test!(path_string_interpolation, || { eval_okay_test!(
unsafe { #[serial(env)]
std::env::set_var("HOME", "/fake-home"); path_string_interpolation,
|| {
// SAFETY: guarded with #[serial_test::serial]
unsafe {
std::env::set_var("HOME", "/fake-home");
}
} }
}); );
eval_okay_test!(patterns); eval_okay_test!(patterns);
eval_okay_test!(print); eval_okay_test!(print);
eval_okay_test!(readDir); eval_okay_test!(readDir);
+8
View File
@@ -1,3 +1,11 @@
#![allow(clippy::allow_attributes_without_reason)]
#![allow(
dead_code,
clippy::unwrap_used,
clippy::unwrap_in_result,
clippy::panic
)]
mod derivation; mod derivation;
mod findfile; mod findfile;
mod io_operations; mod io_operations;
+8 -8
View File
@@ -348,7 +348,7 @@ fn substring_zero_length_empty_value() {
} }
#[test_log::test] #[test_log::test]
#[allow(non_snake_case)] #[expect(non_snake_case)]
fn concatStringsSep_preserves_context() { fn concatStringsSep_preserves_context() {
let result = eval( let result = eval(
r#" r#"
@@ -365,7 +365,7 @@ fn concatStringsSep_preserves_context() {
} }
#[test_log::test] #[test_log::test]
#[allow(non_snake_case)] #[expect(non_snake_case)]
fn concatStringsSep_merges_contexts() { fn concatStringsSep_merges_contexts() {
let result = eval( let result = eval(
r#" r#"
@@ -383,7 +383,7 @@ fn concatStringsSep_merges_contexts() {
} }
#[test_log::test] #[test_log::test]
#[allow(non_snake_case)] #[expect(non_snake_case)]
fn concatStringsSep_separator_has_context() { fn concatStringsSep_separator_has_context() {
let result = eval( let result = eval(
r#" r#"
@@ -398,7 +398,7 @@ fn concatStringsSep_separator_has_context() {
} }
#[test_log::test] #[test_log::test]
#[allow(non_snake_case)] #[expect(non_snake_case)]
fn replaceStrings_input_context_preserved() { fn replaceStrings_input_context_preserved() {
let result = eval( let result = eval(
r#" r#"
@@ -413,7 +413,7 @@ fn replaceStrings_input_context_preserved() {
} }
#[test_log::test] #[test_log::test]
#[allow(non_snake_case)] #[expect(non_snake_case)]
fn replaceStrings_replacement_context_collected() { fn replaceStrings_replacement_context_collected() {
let result = eval( let result = eval(
r#" r#"
@@ -428,7 +428,7 @@ fn replaceStrings_replacement_context_collected() {
} }
#[test_log::test] #[test_log::test]
#[allow(non_snake_case)] #[expect(non_snake_case)]
fn replaceStrings_merges_contexts() { fn replaceStrings_merges_contexts() {
let result = eval( let result = eval(
r#" r#"
@@ -446,7 +446,7 @@ fn replaceStrings_merges_contexts() {
} }
#[test_log::test] #[test_log::test]
#[allow(non_snake_case)] #[expect(non_snake_case)]
fn replaceStrings_lazy_evaluation_context() { fn replaceStrings_lazy_evaluation_context() {
let result = eval( let result = eval(
r#" r#"
@@ -461,7 +461,7 @@ fn replaceStrings_lazy_evaluation_context() {
} }
#[test_log::test] #[test_log::test]
#[allow(non_snake_case)] #[expect(non_snake_case)]
fn baseNameOf_preserves_context() { fn baseNameOf_preserves_context() {
let result = eval( let result = eval(
r#" r#"
-2
View File
@@ -1,5 +1,3 @@
#![allow(dead_code)]
use fix::Evaluator; use fix::Evaluator;
use fix_error::{Result, Source}; use fix_error::{Result, Source};
use fix_lang::Value; use fix_lang::Value;
Generated
+9 -150
View File
@@ -1,63 +1,5 @@
{ {
"nodes": { "nodes": {
"blueprint": {
"inputs": {
"nixpkgs": [
"llm-agents",
"nixpkgs"
],
"systems": [
"llm-agents",
"systems"
]
},
"locked": {
"lastModified": 1776249299,
"narHash": "sha256-Dt9t1TGRmJFc0xVYhttNBD6QsAgHOHCArqGa0AyjrJY=",
"owner": "numtide",
"repo": "blueprint",
"rev": "56131e8628f173d24a27f6d27c0215eff57e40dd",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "blueprint",
"type": "github"
}
},
"bun2nix": {
"inputs": {
"flake-parts": [
"llm-agents",
"flake-parts"
],
"nixpkgs": [
"llm-agents",
"nixpkgs"
],
"systems": [
"llm-agents",
"systems"
],
"treefmt-nix": [
"llm-agents",
"treefmt-nix"
]
},
"locked": {
"lastModified": 1778446047,
"narHash": "sha256-oQvcadh2BCkrog+SGrG6YffKJrveYpjj3TdQJWaKhaM=",
"owner": "nix-community",
"repo": "bun2nix",
"rev": "f2bc12af1a6369648aac41041ceeaa0b866599c6",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "bun2nix",
"type": "github"
}
},
"fenix": { "fenix": {
"inputs": { "inputs": {
"nixpkgs": [ "nixpkgs": [
@@ -66,11 +8,11 @@
"rust-analyzer-src": "rust-analyzer-src" "rust-analyzer-src": "rust-analyzer-src"
}, },
"locked": { "locked": {
"lastModified": 1781343250, "lastModified": 1784017020,
"narHash": "sha256-KBJktAwDG9+10j2wMfvOVkBEhZr3yS769xoqqdFI62s=", "narHash": "sha256-49WO85egjNtN1vMgJ3zUjDh5IqO+ou4zZY80WQ6EKGg=",
"owner": "nix-community", "owner": "nix-community",
"repo": "fenix", "repo": "fenix",
"rev": "aad7d8bb6936d473c2b9d1a5846a1fe1bc92767a", "rev": "fa2a0be0f712d7147d1677d33d15ea7b0589cdb4",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -93,59 +35,13 @@
"url": "https://git.lix.systems/lix-project/flake-compat/archive/main.tar.gz" "url": "https://git.lix.systems/lix-project/flake-compat/archive/main.tar.gz"
} }
}, },
"flake-parts": {
"inputs": {
"nixpkgs-lib": [
"llm-agents",
"nixpkgs"
]
},
"locked": {
"lastModified": 1778716662,
"narHash": "sha256-m1Yf0wZ8j1OHjTc2UwHwyQRSnNeSgLJOd7q5Y45hzi4=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "f7c1a2d347e4c52d5fb8d10cb4d94b5884e546fb",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"llm-agents": {
"inputs": {
"blueprint": "blueprint",
"bun2nix": "bun2nix",
"flake-parts": "flake-parts",
"nixpkgs": [
"nixpkgs"
],
"systems": "systems",
"treefmt-nix": "treefmt-nix"
},
"locked": {
"lastModified": 1781330261,
"narHash": "sha256-2fFAGel2VVXr5mwrTXldqXva2ng3T3HHxyuBKRIxauI=",
"owner": "numtide",
"repo": "llm-agents.nix",
"rev": "24ec6b7b1ddf8896ac8df3b65dc564575e0a1928",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "llm-agents.nix",
"type": "github"
}
},
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1781074563, "lastModified": 1784007870,
"narHash": "sha256-md8WlXOlfnIeHeOScMTTHFyf2d6iaTwPl2apR5EQ3P4=", "narHash": "sha256-djcLt/JJphyNt4eDY9XTly+/WbCK5lqWq9lSgCmJkkQ=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "9ae611a455b90cf061d8f332b977e387bda8e1ca", "rev": "18b9261cb3294b6d2a06d03f96872827b8fe2698",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -159,18 +55,17 @@
"inputs": { "inputs": {
"fenix": "fenix", "fenix": "fenix",
"flake-compat": "flake-compat", "flake-compat": "flake-compat",
"llm-agents": "llm-agents",
"nixpkgs": "nixpkgs" "nixpkgs": "nixpkgs"
} }
}, },
"rust-analyzer-src": { "rust-analyzer-src": {
"flake": false, "flake": false,
"locked": { "locked": {
"lastModified": 1781294997, "lastModified": 1783975201,
"narHash": "sha256-XjCyIvJw4JtcwItTKRdQz5h1pLF9hr8ZSYeMP+/1d3A=", "narHash": "sha256-oyHWTfKWk06nTynCO8ByEAx/KEARvOOyTVnwinOmK3Q=",
"owner": "rust-lang", "owner": "rust-lang",
"repo": "rust-analyzer", "repo": "rust-analyzer",
"rev": "3f92cd1612268995d5667bd04fa03ba2916413d9", "rev": "63a6f0d4bcfd3bbcf36383fcbcbcd93456ed1653",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -179,42 +74,6 @@
"repo": "rust-analyzer", "repo": "rust-analyzer",
"type": "github" "type": "github"
} }
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"treefmt-nix": {
"inputs": {
"nixpkgs": [
"llm-agents",
"nixpkgs"
]
},
"locked": {
"lastModified": 1780220602,
"narHash": "sha256-eynAfOmbmxJnkp7YewvCEbShNnnYJ9gLLqkzsYtBPeM=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "db947814a175b7ca6ded66e21383d938df01c227",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "treefmt-nix",
"type": "github"
}
} }
}, },
"root": "root", "root": "root",
+3 -33
View File
@@ -3,17 +3,13 @@
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
fenix.url = "github:nix-community/fenix"; fenix.url = "github:nix-community/fenix";
fenix.inputs.nixpkgs.follows = "nixpkgs"; fenix.inputs.nixpkgs.follows = "nixpkgs";
llm-agents = {
url = "github:numtide/llm-agents.nix";
inputs.nixpkgs.follows = "nixpkgs";
};
flake-compat = { flake-compat = {
url = "https://git.lix.systems/lix-project/flake-compat/archive/main.tar.gz"; url = "https://git.lix.systems/lix-project/flake-compat/archive/main.tar.gz";
flake = false; flake = false;
}; };
}; };
outputs = outputs =
inputs@{ nixpkgs, fenix, ... }: { nixpkgs, fenix, ... }:
let let
forAllSystems = nixpkgs.lib.genAttrs nixpkgs.lib.systems.flakeExposed; forAllSystems = nixpkgs.lib.genAttrs nixpkgs.lib.systems.flakeExposed;
in in
@@ -24,37 +20,11 @@
pkgs = import nixpkgs { pkgs = import nixpkgs {
inherit system; inherit system;
config.allowUnfree = true; config.allowUnfree = true;
overlays = [ fenix.overlays.default ];
}; };
llm-agents = inputs.llm-agents.packages.${pkgs.stdenv.hostPlatform.system};
in in
{ {
default = pkgs.mkShell { default = import ./devShell.nix { inherit pkgs; };
packages = with pkgs; [
(fenix.packages.${system}.latest.withComponents [
"cargo"
"clippy"
"rust-src"
"rustc"
"rustfmt"
"rust-analyzer"
])
cargo-machete
cargo-bloat
lldb
valgrind
kdePackages.kcachegrind
hyperfine
just
samply
tokei
tombi
# llm-agents.codex
llm-agents.claude-code
llm-agents.opencode
# llm-agents.forge
];
};
} }
); );
}; };
-16
View File
@@ -1,16 +0,0 @@
let
lockFile = builtins.fromJSON (builtins.readFile ./flake.lock);
flake-compat-node = lockFile.nodes.${lockFile.nodes.root.inputs.flake-compat};
flake-compat = builtins.fetchTarball {
inherit (flake-compat-node.locked) url;
sha256 = flake-compat-node.locked.narHash;
};
flake = (
import flake-compat {
src = ./.;
copySourceTreeToStore = false;
}
);
in
flake.shellNix