Compare commits

...
18 Commits
Author SHA1 Message Date
imxyy1soope1 0e344b1097 macros, vm, bytecode: add #[primop] await-style primop macro 2026-08-23 17:38:08 +08:00
imxyy1soope1 89f5d84009 deps: update dependencies 2026-08-22 20:59:43 +08:00
imxyy1soope1 c0f2d1a20a vm, runtime: add slots! macro and port filter primop to typed slots 2026-08-22 20:59:43 +08:00
imxyy1soope1 5a06fb4c1e runtime, vm: write thunk results back to the forcing stack slot 2026-08-22 19:05:13 +08:00
imxyy1soope1 0c0e1849ae runtime: make ValueVariant lifetime-generic and add TryFrom<Value> conversions 2026-08-22 18:49:40 +08:00
imxyy1soope1 cb38031f85 treewide: enforce strict clippy check 2026-08-22 18:41:23 +08:00
imxyy1soope1 5f494983b8 Justfile: drop JS-era inspector/profiling recipes 2026-08-22 18:36:34 +08:00
imxyy1soope1 ed312f7919 flake: extract devShell.nix and drop flake-compat shell.nix 2026-08-22 18:36:34 +08:00
imxyy1soope1 11622bff22 flake.lock: update 2026-07-15 19:11:03 +08:00
imxyy1soope1 09ee923903 runtime, macros: unify NaN-boxed value API under ValueVariant trait 2026-07-15 19:11:03 +08:00
imxyy1soope1 7220b42024 rename PrimOpPhase 2026-07-12 21:11:38 +08:00
imxyy1soope1 dde3052e2d chore 2026-06-30 18:49:45 +08:00
imxyy1soope1 f0e3f1eeca fix-vm: use Machine trait exclusively 2026-06-19 22:37:29 +08:00
imxyy1soope1 afbc471e40 *.toml: reformat using tombi 2026-06-19 21:17:10 +08:00
imxyy1soope1 c1b4ac4d8f flake.lock: update 2026-06-13 23:24:08 +08:00
imxyy1soope1 81ac08fb5a refactor: reorganize crate hierarchy 2026-06-06 22:02:31 +08:00
imxyy1soope1 9412c319f9 implement any & all 2026-05-20 21:05:36 +08:00
imxyy1soope1 b420a950a3 chore: flake.nix 2026-05-20 21:04:41 +08:00
78 changed files with 6264 additions and 4688 deletions
Generated
+340 -432
View File
File diff suppressed because it is too large Load Diff
+90 -29
View File
@@ -2,41 +2,102 @@
resolver = "3" resolver = "3"
members = [ members = [
"fix", "fix",
"fix-abstract-vm", "fix-bytecode",
"fix-builtins", "fix-compiler",
"fix-codegen",
"fix-common",
"fix-error", "fix-error",
"fix-ir", "fix-lang",
"fix-primops", "fix-macros",
"fix-runtime",
"fix-vm", "fix-vm",
] ]
[profile.profiling] [workspace.dependencies]
inherits = "release" bumpalo = {
debug = true version = "3.20",
features = [
"allocator-api2",
"boxed",
"collections",
]
}
ere = "0.2"
ghost-cell = "0.2"
hashbrown = "0.17"
num_enum = "0.7"
rnix = "0.14"
rowan = "0.16"
smallvec = { version = "1.15", features = ["const_generics", "const_new"] }
string-interner = "0.20"
gc-arena = { version = "0.7", features = ["hashbrown", "smallvec"] }
# https://emschwartz.me/your-clippy-config-should-be-stricter/
# tombi: format.rules.table-keys-order.disabled = true
[workspace.lints.clippy]
# 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"
lto = true lto = true
[workspace.dependencies] [profile.profiling]
bumpalo = { version = "3.20", features = [ inherits = "release"
"allocator-api2", debug = true
"boxed",
"collections",
] }
ghost-cell = "0.2"
hashbrown = "0.16"
num_enum = "0.7.5"
smallvec = { version = "1.15", features = ["const_new", "const_generics"] }
ere = "0.2"
string-interner = "0.19"
rnix = "0.14"
rowan = "0.16"
likely_stable = "0.1"
[workspace.dependencies.gc-arena]
git = "https://github.com/kyren/gc-arena"
rev = "75671ae03f53718357b741ed4027560f14e90836"
features = ["allocator-api2", "hashbrown", "smallvec"]
-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
];
}
-142
View File
@@ -1,142 +0,0 @@
#![allow(dead_code)]
use fix_codegen::OperandType;
use fix_common::StringId;
use num_enum::TryFromPrimitive;
use string_interner::Symbol as _;
use crate::{OperandData, VmRuntimeCtx};
pub struct BytecodeReader<'a> {
bytecode: &'a [u8],
pc: usize,
inst_start_pc: usize,
}
impl<'a> BytecodeReader<'a> {
pub fn new(bytecode: &'a [u8], pc: usize) -> Self {
Self {
bytecode,
pc,
inst_start_pc: pc,
}
}
#[inline(always)]
pub fn from_after_op(bytecode: &'a [u8], inst_start_pc: usize) -> Self {
Self {
bytecode,
pc: inst_start_pc + 1,
inst_start_pc,
}
}
#[inline(always)]
#[cfg_attr(debug_assertions, track_caller)]
fn read_array<const N: usize>(&mut self) -> [u8; N] {
let ret = self.bytecode[self.pc..self.pc + N]
.try_into()
.expect("read_array failed");
self.pc += N;
ret
}
#[inline(always)]
pub fn read_op(&mut self) -> fix_codegen::Op {
use fix_codegen::Op;
self.inst_start_pc = self.pc;
let byte = self.bytecode[self.pc];
if !likely_stable::likely((0..Op::Illegal as u8).contains(&byte)) {
panic!("unknown opcode: {byte:#04x}")
}
self.pc += 1;
unsafe { std::mem::transmute::<u8, Op>(byte) }
}
#[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<C: VmRuntimeCtx>(&mut self, ctx: &C) -> 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 => {
let id = self.read_u32();
OperandData::Const(ctx.get_const(id))
}
OperandType::BigInt => {
let val = self.read_i64();
OperandData::BigInt(val)
}
OperandType::Local => {
let layer = self.read_u8();
let idx = self.read_u32();
OperandData::Local { layer, idx }
}
OperandType::BuiltinConst => {
let id = self.read_string_id();
OperandData::BuiltinConst(id)
}
OperandType::Builtins => OperandData::Builtins,
OperandType::ReplBinding => {
let id = self.read_string_id();
OperandData::ReplBinding(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 {
self.pc
}
pub fn set_pc(&mut self, pc: usize) {
self.pc = pc;
}
pub fn inst_start_pc(&self) -> usize {
self.inst_start_pc
}
}
-8
View File
@@ -1,8 +0,0 @@
[package]
name = "fix-builtins"
version = "0.1.0"
edition = "2024"
[dependencies]
num_enum = { workspace = true }
gc-arena = { workspace = true }
-399
View File
@@ -1,399 +0,0 @@
use gc_arena::Collect;
use num_enum::TryFromPrimitive;
macro_rules! define_builtins {
($(($name:literal, $variant:ident, $arity:expr)),* $(,)?) => {
/// Builtin function registry.
/// Array index IS the PrimOp id. (name, arity) pairs.
pub const BUILTINS: &[(&str, u8)] = &[
$(($name, $arity),)*
];
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, TryFromPrimitive, Collect)]
#[repr(u8)]
#[collect(require_static)]
pub enum BuiltinId {
$($variant,)*
}
};
}
define_builtins! {
("abort", Abort, 1),
("__add", Add, 2),
("__addErrorContext", AddErrorContext, 2),
("__all", All, 2),
("__any", Any, 2),
("__appendContext", AppendContext, 2),
("__attrNames", AttrNames, 1),
("__attrValues", AttrValues, 1),
("baseNameOf", BaseNameOf, 1),
("__bitAnd", BitAnd, 2),
("__bitOr", BitOr, 2),
("__bitXor", BitXor, 2),
("break", Break, 1),
("__catAttrs", CatAttrs, 2),
("__ceil", Ceil, 1),
("__compareVersions", CompareVersions, 2),
("__concatLists", ConcatLists, 1),
("__concatMap", ConcatMap, 2),
("__concatStringsSep", ConcatStringsSep, 2),
("__convertHash", ConvertHash, 1),
("__deepSeq", DeepSeq, 2),
("derivation", Derivation, 1),
("derivationStrict", DerivationStrict, 1),
("dirOf", DirOf, 1),
("__div", Div, 2),
("__elem", Elem, 2),
("__elemAt", ElemAt, 2),
("fetchGit", FetchGit, 1),
("fetchMercurial", FetchMercurial, 1),
("fetchTarball", FetchTarball, 1),
("fetchTree", FetchTree, 1),
("__fetchurl", FetchUrl, 1),
("__filter", Filter, 2),
("__filterSource", FilterSource, 2),
("__findFile", FindFile, 2),
("__floor", Floor, 1),
("__foldl'", FoldlStrict, 3),
("__fromJSON", FromJSON, 1),
("fromTOML", FromTOML, 1),
("__functionArgs", FunctionArgs, 1),
("__genList", GenList, 2),
("__genericClosure", GenericClosure, 1),
("__getAttr", GetAttr, 2),
("__getContext", GetContext, 1),
("__getEnv", GetEnv, 1),
("__groupBy", GroupBy, 2),
("__hasAttr", HasAttr, 2),
("__hasContext", HasContext, 1),
("__hashFile", HashFile, 2),
("__hashString", HashString, 2),
("__head", Head, 1),
("import", Import, 1),
("__intersectAttrs", IntersectAttrs, 2),
("__isAttrs", IsAttrs, 1),
("__isBool", IsBool, 1),
("__isFloat", IsFloat, 1),
("__isFunction", IsFunction, 1),
("__isInt", IsInt, 1),
("__isList", IsList, 1),
("isNull", IsNull, 1),
("__isPath", IsPath, 1),
("__isString", IsString, 1),
("__length", Length, 1),
("__lessThan", LessThan, 2),
("__listToAttrs", ListToAttrs, 1),
("map", Map, 2),
("__mapAttrs", MapAttrs, 2),
("__match", Match, 2),
("__mul", Mul, 2),
("__parseDrvName", ParseDrvName, 1),
("__partition", Partition, 2),
("__path", Path, 1),
("__pathExists", PathExists, 1),
("placeholder", Placeholder, 1),
("__readDir", ReadDir, 1),
("__readFile", ReadFile, 1),
("__readFileType", ReadFileType, 1),
("removeAttrs", RemoveAttrs, 2),
("__replaceStrings", ReplaceStrings, 3),
("scopedImport", ScopedImport, 2),
("__seq", Seq, 2),
("__sort", Sort, 2),
("__split", Split, 2),
("__splitVersion", SplitVersion, 1),
("__storePath", StorePath, 1),
("__stringLength", StringLength, 1),
("__sub", Sub, 2),
("__substring", Substring, 3),
("__tail", Tail, 1),
("throw", Throw, 1),
("__toFile", ToFile, 2),
("__toJSON", ToJSON, 1),
("__toPath", ToPath, 1),
("toString", ToString, 1),
("__toXML", ToXML, 1),
("__trace", Trace, 2),
("__tryEval", TryEval, 1),
("__typeOf", TypeOf, 1),
("__unsafeDiscardStringContext", UnsafeDiscardStringContext, 1),
("__unsafeDiscardOutputDependency", UnsafeDiscardOutputDependency, 1),
("__unsafeGetAttrPos", UnsafeGetAttrPos, 2),
("__warn", Warn, 2),
("__zipAttrsWith", ZipAttrsWith, 2),
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
pub enum PrimOpPhase {
Abort,
Add,
AddErrorContext,
All,
Any,
AppendContext,
AttrNames,
AttrValues,
BaseNameOf,
BitAnd,
BitOr,
BitXor,
Break,
CatAttrs,
Ceil,
CompareVersions,
ConcatLists,
ConcatMap,
ConcatStringsSep,
ConvertHash,
DeepSeq,
DeepSeqPush,
DeepSeqLoop,
Derivation,
DerivationStrict,
DirOf,
Div,
Elem,
ElemAt,
FetchGit,
FetchMercurial,
FetchTarball,
FetchTree,
FetchUrl,
FilterForceList,
FilterCallPred,
FilterCheck,
FilterSource,
FindFile,
Floor,
FoldlStrict,
FoldlStrictEmpty,
FoldlStrictCall1,
FoldlStrictCall2,
FoldlStrictUpdate,
FromJSON,
FromTOML,
FunctionArgs,
GenList,
GenericClosure,
GetAttr,
GetContext,
GetEnv,
GroupBy,
HasAttr,
HasContext,
HashFile,
HashString,
Head,
Import,
IntersectAttrs,
IsAttrs,
IsBool,
IsFloat,
IsFunction,
IsInt,
IsList,
IsNull,
IsPath,
IsString,
Length,
LessThan,
ListToAttrs,
Map,
MapAttrs,
Match,
Mul,
ParseDrvName,
Partition,
Path,
PathExists,
Placeholder,
ReadDir,
ReadFile,
ReadFileType,
RemoveAttrs,
ReplaceStrings,
ScopedImport,
Seq,
Sort,
Split,
SplitVersion,
StorePath,
StringLength,
Sub,
Substring,
Tail,
Throw,
ToFile,
ToJSON,
ToPath,
ToString,
ToXML,
Trace,
TryEval,
TypeOf,
UnsafeDiscardStringContext,
UnsafeGetAttrPos,
Warn,
ZipAttrsWith,
ForceResultShallow,
ForceResultShallowPush,
ForceResultShallowLoop,
ForceResultDeepFinish,
EqStep,
EqForce,
// TODO: split into separate enums
CallPattern,
CallFunctor1,
CallFunctor2,
ImportFinalize,
ScopedImportFinalize,
AppendContextLoop,
AppendContextEntryForced,
AppendContextOutputsForced,
AppendContextOutputElementLoop,
AppendContextOutputElementForced,
UnsafeDiscardOutputDependency,
Illegal,
}
impl TryFrom<u8> for PrimOpPhase {
type Error = u8;
fn try_from(value: u8) -> Result<Self, Self::Error> {
if (0..Self::Illegal as u8).contains(&value) {
Ok(unsafe { std::mem::transmute::<u8, Self>(value) })
} else {
Err(value)
}
}
}
impl BuiltinId {
#[inline(always)]
pub fn entry_phase(self) -> PrimOpPhase {
use BuiltinId::*;
match self {
Abort => PrimOpPhase::Abort,
Add => PrimOpPhase::Add,
AddErrorContext => PrimOpPhase::AddErrorContext,
All => PrimOpPhase::All,
Any => PrimOpPhase::Any,
AppendContext => PrimOpPhase::AppendContext,
AttrNames => PrimOpPhase::AttrNames,
AttrValues => PrimOpPhase::AttrValues,
BaseNameOf => PrimOpPhase::BaseNameOf,
BitAnd => PrimOpPhase::BitAnd,
BitOr => PrimOpPhase::BitOr,
BitXor => PrimOpPhase::BitXor,
Break => PrimOpPhase::Break,
CatAttrs => PrimOpPhase::CatAttrs,
Ceil => PrimOpPhase::Ceil,
CompareVersions => PrimOpPhase::CompareVersions,
ConcatLists => PrimOpPhase::ConcatLists,
ConcatMap => PrimOpPhase::ConcatMap,
ConcatStringsSep => PrimOpPhase::ConcatStringsSep,
ConvertHash => PrimOpPhase::ConvertHash,
DeepSeq => PrimOpPhase::DeepSeq,
Derivation => PrimOpPhase::Derivation,
DerivationStrict => PrimOpPhase::DerivationStrict,
DirOf => PrimOpPhase::DirOf,
Div => PrimOpPhase::Div,
Elem => PrimOpPhase::Elem,
ElemAt => PrimOpPhase::ElemAt,
FetchGit => PrimOpPhase::FetchGit,
FetchMercurial => PrimOpPhase::FetchMercurial,
FetchTarball => PrimOpPhase::FetchTarball,
FetchTree => PrimOpPhase::FetchTree,
FetchUrl => PrimOpPhase::FetchUrl,
Filter => PrimOpPhase::FilterForceList,
FilterSource => PrimOpPhase::FilterSource,
FindFile => PrimOpPhase::FindFile,
Floor => PrimOpPhase::Floor,
FoldlStrict => PrimOpPhase::FoldlStrict,
FromJSON => PrimOpPhase::FromJSON,
FromTOML => PrimOpPhase::FromTOML,
FunctionArgs => PrimOpPhase::FunctionArgs,
GenList => PrimOpPhase::GenList,
GenericClosure => PrimOpPhase::GenericClosure,
GetAttr => PrimOpPhase::GetAttr,
GetContext => PrimOpPhase::GetContext,
GetEnv => PrimOpPhase::GetEnv,
GroupBy => PrimOpPhase::GroupBy,
HasAttr => PrimOpPhase::HasAttr,
HasContext => PrimOpPhase::HasContext,
HashFile => PrimOpPhase::HashFile,
HashString => PrimOpPhase::HashString,
Head => PrimOpPhase::Head,
Import => PrimOpPhase::Import,
IntersectAttrs => PrimOpPhase::IntersectAttrs,
IsAttrs => PrimOpPhase::IsAttrs,
IsBool => PrimOpPhase::IsBool,
IsFloat => PrimOpPhase::IsFloat,
IsFunction => PrimOpPhase::IsFunction,
IsInt => PrimOpPhase::IsInt,
IsList => PrimOpPhase::IsList,
IsNull => PrimOpPhase::IsNull,
IsPath => PrimOpPhase::IsPath,
IsString => PrimOpPhase::IsString,
Length => PrimOpPhase::Length,
LessThan => PrimOpPhase::LessThan,
ListToAttrs => PrimOpPhase::ListToAttrs,
Map => PrimOpPhase::Map,
MapAttrs => PrimOpPhase::MapAttrs,
Match => PrimOpPhase::Match,
Mul => PrimOpPhase::Mul,
ParseDrvName => PrimOpPhase::ParseDrvName,
Partition => PrimOpPhase::Partition,
Path => PrimOpPhase::Path,
PathExists => PrimOpPhase::PathExists,
Placeholder => PrimOpPhase::Placeholder,
ReadDir => PrimOpPhase::ReadDir,
ReadFile => PrimOpPhase::ReadFile,
ReadFileType => PrimOpPhase::ReadFileType,
RemoveAttrs => PrimOpPhase::RemoveAttrs,
ReplaceStrings => PrimOpPhase::ReplaceStrings,
ScopedImport => PrimOpPhase::ScopedImport,
Seq => PrimOpPhase::Seq,
Sort => PrimOpPhase::Sort,
Split => PrimOpPhase::Split,
SplitVersion => PrimOpPhase::SplitVersion,
StorePath => PrimOpPhase::StorePath,
StringLength => PrimOpPhase::StringLength,
Sub => PrimOpPhase::Sub,
Substring => PrimOpPhase::Substring,
Tail => PrimOpPhase::Tail,
Throw => PrimOpPhase::Throw,
ToFile => PrimOpPhase::ToFile,
ToJSON => PrimOpPhase::ToJSON,
ToPath => PrimOpPhase::ToPath,
ToString => PrimOpPhase::ToString,
ToXML => PrimOpPhase::ToXML,
Trace => PrimOpPhase::Trace,
TryEval => PrimOpPhase::TryEval,
TypeOf => PrimOpPhase::TypeOf,
UnsafeDiscardStringContext => PrimOpPhase::UnsafeDiscardStringContext,
UnsafeDiscardOutputDependency => PrimOpPhase::UnsafeDiscardOutputDependency,
UnsafeGetAttrPos => PrimOpPhase::UnsafeGetAttrPos,
Warn => PrimOpPhase::Warn,
ZipAttrsWith => PrimOpPhase::ZipAttrsWith,
}
}
}
impl PrimOpPhase {
pub fn ip(self) -> u32 {
self as u32 * 2
}
}
+14
View File
@@ -0,0 +1,14 @@
[package]
name = "fix-bytecode"
version = "0.1.0"
edition = "2024"
[dependencies]
colored = "3.1.1"
num_enum = { workspace = true }
string-interner = { workspace = true }
fix-lang = { path = "../fix-lang" }
[lints]
workspace = true
@@ -1,10 +1,8 @@
use std::fmt::Write; use std::fmt::Write;
use colored::Colorize; use colored::Colorize as _;
use fix_builtins::BuiltinId;
use num_enum::TryFromPrimitive;
use crate::{InstructionPtr, Op, OperandType}; 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;
@@ -12,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_primitive(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();
}
} }
} }
@@ -116,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 {
@@ -124,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);
} }
@@ -201,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_primitive(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))
} }
@@ -234,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={} ...={})",
@@ -274,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(']');
@@ -293,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::DispatchPrimOp => { Op::DispatchCont => {
let id = BuiltinId::try_from_primitive(self.read_u8()).expect("invalid builtin id"); let phase =
("DispatchPrimOp", format!("id={id:?}")) Continuation::try_from(self.reader.read_u8()).expect("invalid primop 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(']');
} }
@@ -326,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)),
@@ -359,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",
@@ -367,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",
@@ -376,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))
} }
@@ -400,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",
@@ -408,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))
} }
+614
View File
@@ -0,0 +1,614 @@
#![allow(
dead_code,
reason = "crate is under active development; some opcodes and helpers are not yet wired up"
)]
use fix_lang::{BuiltinId, StringId};
use num_enum::TryFromPrimitive;
use string_interner::Symbol as _;
pub mod disassembler;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct InstructionPtr(pub usize);
#[repr(u8)]
#[derive(Debug, Clone, Copy)]
pub enum Op {
PushSmi,
PushBigInt,
PushFloat,
PushString,
PushNull,
PushTrue,
PushFalse,
LoadLocal,
LoadOuter,
StoreLocal,
AllocLocals,
MakeThunk,
MakeClosure,
MakePatternClosure,
Call,
DispatchCont,
MakeAttrs,
MakeEmptyAttrs,
SelectStatic,
SelectDynamic,
HasAttrPathStatic,
HasAttrPathDynamic,
HasAttrStatic,
HasAttrDynamic,
HasAttrResolve,
JumpIfSelectSucceeded,
JumpIfSelectFailed,
MakeList,
MakeEmptyList,
OpAdd,
OpSub,
OpMul,
OpDiv,
OpEq,
OpNeq,
OpLt,
OpGt,
OpLeq,
OpGeq,
OpConcat,
OpUpdate,
OpNeg,
OpNot,
JumpIfFalse,
JumpIfTrue,
Jump,
CoerceToString,
ConcatStrings,
ResolvePath,
Assert,
LookupWith,
LoadBuiltins,
LoadBuiltin,
LoadReplBinding,
LoadScopedBinding,
Return,
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)]
#[derive(Debug, Clone, Copy, TryFromPrimitive)]
pub enum OperandType {
Const,
BigInt,
Local,
BuiltinConst,
Builtins,
ReplBinding,
ScopedImportBinding,
}
pub enum Const {
Smi(i32),
Float(f64),
Bool(bool),
String(StringId),
Path(StringId),
PrimOp(BuiltinId),
Null,
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, TryFromPrimitive)]
pub enum AttrKeyType {
Static,
Dynamic,
}
pub enum OperandData {
Const(u32),
BigInt(i64),
Local { layer: u8, idx: u32 },
BuiltinConst(StringId),
Builtins,
ReplBinding(StringId),
ScopedImportBinding { slot_id: u32, name: StringId },
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
pub enum Continuation {
// primops
PAbort,
PAdd,
PAddErrorContext,
PAll0,
PAll1,
PAll2,
PAll3,
PAny0,
PAny1,
PAny2,
PAny3,
PAppendContext,
PAppendContextLoop,
PAppendContextEntryForced,
PAppendContextOutputsForced,
PAppendContextOutputElementLoop,
PAppendContextOutputElementForced,
PAttrNames,
PAttrValues,
PBaseNameOf,
PBitAnd,
PBitOr,
PBitXor,
PBreak,
PCatAttrs,
PCeil,
PCompareVersions,
PConcatLists,
PConcatMap,
PConcatStringsSep,
PConvertHash,
PDeepSeq0,
PDeepSeq1,
PDeepSeq2,
PDeepSeq3,
PDeepSeq4,
PDeepSeq5,
PDerivation,
PDerivationStrict,
PDirOf,
PDiv,
PElem,
PElemAt,
PFetchGit,
PFetchMercurial,
PFetchTarball,
PFetchTree,
PFetchUrl,
PFilter0,
PFilter1,
PFilter2,
PFilter3,
PFilter4,
PFilterSource,
PFindFile,
PFloor,
PFoldlStrict0,
PFoldlStrict1,
PFoldlStrict2,
PFoldlStrict3,
PFoldlStrict4,
PFoldlStrict5,
PFromJSON,
PFromTOML,
PFunctionArgs,
PGenList,
PGenericClosure,
PGetAttr,
PGetContext,
PGetEnv,
PGroupBy,
PHasAttr,
PHasContext,
PHashFile,
PHashString,
PHead,
PImport,
PImportFinalize,
PScopedImport,
PScopedImportFinalize,
PIntersectAttrs,
PIsAttrs,
PIsBool,
PIsFloat,
PIsFunction,
PIsInt,
PIsList,
PIsNull,
PIsPath,
PIsString,
PLength,
PLessThan,
PListToAttrs,
PMap,
PMapAttrs,
PMatch,
PMul,
PParseDrvName,
PPartition,
PPath,
PPathExists,
PPlaceholder,
PReadDir,
PReadFile,
PReadFileType,
PRemoveAttrs,
PReplaceStrings,
PSeq0,
PSeq1,
PSort,
PSplit,
PSplitVersion,
PStorePath,
PStringLength,
PSub,
PSubstring,
PTail,
PThrow,
PToFile,
PToJSON,
PToPath,
PToString,
PToXML,
PTrace,
PTryEval,
PTypeOf,
PUnsafeDiscardStringContext,
PUnsafeGetAttrPos,
PWarn,
PZipAttrsWith,
PUnsafeDiscardOutputDependency,
ForceResultShallow,
ForceResultShallowPush,
ForceResultShallowLoop,
ForceResultDeepFinish,
EqStep,
EqForce,
CallPattern,
CallFunctor1,
CallFunctor2,
Illegal,
}
impl TryFrom<u8> for Continuation {
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)
}
}
}
impl Continuation {
pub const fn entry_for_builtin(id: BuiltinId) -> Self {
use BuiltinId::*;
match id {
Abort => Self::PAbort,
Add => Self::PAdd,
AddErrorContext => Self::PAddErrorContext,
All => Self::PAll0,
Any => Self::PAny0,
AppendContext => Self::PAppendContext,
AttrNames => Self::PAttrNames,
AttrValues => Self::PAttrValues,
BaseNameOf => Self::PBaseNameOf,
BitAnd => Self::PBitAnd,
BitOr => Self::PBitOr,
BitXor => Self::PBitXor,
Break => Self::PBreak,
CatAttrs => Self::PCatAttrs,
Ceil => Self::PCeil,
CompareVersions => Self::PCompareVersions,
ConcatLists => Self::PConcatLists,
ConcatMap => Self::PConcatMap,
ConcatStringsSep => Self::PConcatStringsSep,
ConvertHash => Self::PConvertHash,
DeepSeq => Self::PDeepSeq0,
Derivation => Self::PDerivation,
DerivationStrict => Self::PDerivationStrict,
DirOf => Self::PDirOf,
Div => Self::PDiv,
Elem => Self::PElem,
ElemAt => Self::PElemAt,
FetchGit => Self::PFetchGit,
FetchMercurial => Self::PFetchMercurial,
FetchTarball => Self::PFetchTarball,
FetchTree => Self::PFetchTree,
FetchUrl => Self::PFetchUrl,
Filter => Self::PFilter0,
FilterSource => Self::PFilterSource,
FindFile => Self::PFindFile,
Floor => Self::PFloor,
FoldlStrict => Self::PFoldlStrict0,
FromJSON => Self::PFromJSON,
FromTOML => Self::PFromTOML,
FunctionArgs => Self::PFunctionArgs,
GenList => Self::PGenList,
GenericClosure => Self::PGenericClosure,
GetAttr => Self::PGetAttr,
GetContext => Self::PGetContext,
GetEnv => Self::PGetEnv,
GroupBy => Self::PGroupBy,
HasAttr => Self::PHasAttr,
HasContext => Self::PHasContext,
HashFile => Self::PHashFile,
HashString => Self::PHashString,
Head => Self::PHead,
Import => Self::PImport,
IntersectAttrs => Self::PIntersectAttrs,
IsAttrs => Self::PIsAttrs,
IsBool => Self::PIsBool,
IsFloat => Self::PIsFloat,
IsFunction => Self::PIsFunction,
IsInt => Self::PIsInt,
IsList => Self::PIsList,
IsNull => Self::PIsNull,
IsPath => Self::PIsPath,
IsString => Self::PIsString,
Length => Self::PLength,
LessThan => Self::PLessThan,
ListToAttrs => Self::PListToAttrs,
Map => Self::PMap,
MapAttrs => Self::PMapAttrs,
Match => Self::PMatch,
Mul => Self::PMul,
ParseDrvName => Self::PParseDrvName,
Partition => Self::PPartition,
Path => Self::PPath,
PathExists => Self::PPathExists,
Placeholder => Self::PPlaceholder,
ReadDir => Self::PReadDir,
ReadFile => Self::PReadFile,
ReadFileType => Self::PReadFileType,
RemoveAttrs => Self::PRemoveAttrs,
ReplaceStrings => Self::PReplaceStrings,
ScopedImport => Self::PScopedImport,
Seq => Self::PSeq0,
Sort => Self::PSort,
Split => Self::PSplit,
SplitVersion => Self::PSplitVersion,
StorePath => Self::PStorePath,
StringLength => Self::PStringLength,
Sub => Self::PSub,
Substring => Self::PSubstring,
Tail => Self::PTail,
Throw => Self::PThrow,
ToFile => Self::PToFile,
ToJSON => Self::PToJSON,
ToPath => Self::PToPath,
ToString => Self::PToString,
ToXML => Self::PToXML,
Trace => Self::PTrace,
TryEval => Self::PTryEval,
TypeOf => Self::PTypeOf,
UnsafeDiscardStringContext => Self::PUnsafeDiscardStringContext,
UnsafeDiscardOutputDependency => Self::PUnsafeDiscardOutputDependency,
UnsafeGetAttrPos => Self::PUnsafeGetAttrPos,
Warn => Self::PWarn,
ZipAttrsWith => Self::PZipAttrsWith,
}
}
pub const fn ip(self) -> u32 {
self as u32 * 2
}
}
pub struct BytecodeReader<'a> {
bytecode: &'a [u8],
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> {
#[must_use]
pub fn new(bytecode: &'a [u8], pc: usize) -> Self {
Self {
bytecode,
pc,
inst_start_pc: pc,
}
}
#[inline(always)]
#[must_use]
pub fn from_after_op(bytecode: &'a [u8], inst_start_pc: usize) -> Self {
Self {
bytecode,
pc: inst_start_pc + 1,
inst_start_pc,
}
}
#[inline(always)]
#[must_use]
pub fn read<T: FromBytecode>(&mut self) -> T {
T::read(self)
}
read_aliases! {
read_op -> Op,
read_u8 -> u8,
read_u16 -> u16,
read_u32 -> u32,
read_i32 -> i32,
read_i64 -> i64,
read_f64 -> f64,
read_string_id -> StringId,
read_operand_data -> OperandData,
}
pub fn pc(&self) -> usize {
self.pc
}
pub fn set_pc(&mut self, pc: usize) {
self.pc = pc;
}
pub fn inst_start_pc(&self) -> usize {
self.inst_start_pc
}
}
-15
View File
@@ -1,15 +0,0 @@
[package]
name = "fix-codegen"
version = "0.1.0"
edition = "2024"
[dependencies]
hashbrown = { workspace = true }
num_enum = { workspace = true }
rnix = { workspace = true }
string-interner = { workspace = true }
colored = "3.1.1"
fix-builtins = { path = "../fix-builtins" }
fix-common = { path = "../fix-common" }
fix-ir = { path = "../fix-ir" }
+10 -5
View File
@@ -1,17 +1,22 @@
[package] [package]
name = "fix-ir" name = "fix-compiler"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
bumpalo = { workspace = true } bumpalo = { workspace = true }
colored = "3.1.1"
ghost-cell = { workspace = true } ghost-cell = { workspace = true }
hashbrown = { workspace = true }
rnix = { workspace = true } rnix = { workspace = true }
rowan = { workspace = true } rowan = { workspace = true }
string-interner = { workspace = true } string-interner = { workspace = true }
hashbrown = { workspace = true }
num_enum = { workspace = true }
fix-builtins = { path = "../fix-builtins" } fix-bytecode = { path = "../fix-bytecode" }
fix-common = { path = "../fix-common" }
fix-error = { path = "../fix-error" } fix-error = { path = "../fix-error" }
fix-lang = { path = "../fix-lang" }
fix-runtime = { path = "../fix-runtime" }
tracing = "0.1"
[lints]
workspace = true
+570
View File
@@ -0,0 +1,570 @@
use bumpalo::Bump;
use fix_bytecode::{Const, Continuation, InstructionPtr, Op};
use fix_error::{Error, Result, Source};
use fix_lang::{StringId, Symbol};
use fix_runtime::{StaticValue, VmCode, VmRuntimeCtx};
use ghost_cell::{GhostCell, GhostToken};
use hashbrown::{HashMap, HashSet};
use string_interner::DefaultStringInterner;
use crate::BytecodeContext;
use crate::ir::downgrade::{Downgrade as _, DowngradeContext};
use crate::ir::{
GhostMaybeThunkRef, GhostRoIrRef, GhostRoMaybeThunkRef, GhostRoRef, Ir, MaybeThunk, RawIrRef,
ThunkId,
};
pub struct CodeState {
pub bytecode: Vec<u8>,
pub sources: Vec<Source>,
pub spans: Vec<(usize, rnix::TextRange)>,
pub thunk_count: usize,
pub global_env: HashMap<StringId, MaybeThunk>,
}
impl CodeState {
pub fn new(strings: &mut DefaultStringInterner) -> Self {
let global_env = crate::ir::new_global_env(strings);
let mut bytecode = Vec::with_capacity(Continuation::Illegal as usize * 2);
for phase in 0..=Continuation::Illegal as u8 {
bytecode.push(Op::DispatchCont as u8);
bytecode.push(phase);
}
Self {
sources: Vec::new(),
spans: Vec::new(),
thunk_count: 0,
bytecode,
global_env,
}
}
pub fn compile_bytecode<'ctx>(
&'ctx mut self,
source: Source,
extra_scope: Option<ExtraScope<'ctx>>,
runtime: &'ctx mut impl VmRuntimeCtx,
) -> Result<InstructionPtr> {
let mut compiler = CompilerCtx {
code: self,
runtime,
};
compiler.compile_bytecode(source, extra_scope)
}
}
impl VmCode for CodeState {
fn bytecode(&self) -> &[u8] {
&self.bytecode
}
fn compile_with_scope(
&mut self,
source: Source,
extra_scope: Option<fix_runtime::ExtraScope>,
runtime: &mut impl VmRuntimeCtx,
) -> Result<InstructionPtr> {
let extra = extra_scope.map(|s| match s {
fix_runtime::ExtraScope::ScopedImport { keys, slot_id } => {
ExtraScope::ScopedImport { keys, slot_id }
}
});
CodeState::compile_bytecode(self, source, extra, runtime)
}
}
struct CompilerCtx<'a, R: VmRuntimeCtx> {
code: &'a mut CodeState,
runtime: &'a mut R,
}
impl<'a, R: VmRuntimeCtx> CompilerCtx<'a, R> {
fn compile_bytecode(
&mut self,
source: Source,
extra_scope: Option<ExtraScope>,
) -> Result<InstructionPtr> {
let root = self.downgrade(source, extra_scope)?;
let ip = crate::compile_bytecode(root.as_ref(), self);
Ok(ip)
}
fn downgrade(&mut self, source: Source, extra_scope: Option<ExtraScope>) -> Result<OwnedIr> {
tracing::debug!("Parsing Nix expression");
self.code.sources.push(source.clone());
let root = rnix::Root::parse(&source.src);
handle_parse_error(root.errors(), source.clone()).map_or(Ok(()), Err)?;
tracing::debug!("Downgrading Nix expression");
let expr = root
.tree()
.expr()
.ok_or_else(|| Error::parse_error("unexpected EOF".into()))?;
let bump = Bump::new();
GhostToken::new(|token| {
let downgrade_ctx = DowngradeCtx::new(
&bump,
token,
self.runtime,
&self.code.global_env,
extra_scope.map(Into::into),
&mut self.code.thunk_count,
source,
);
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) };
// SAFETY: `ir` borrows from `bump`
Ok(unsafe { OwnedIr::new(bump, ir) })
})
}
}
impl<'a, R: VmRuntimeCtx> BytecodeContext for CompilerCtx<'a, R> {
fn intern_string(&mut self, s: &str) -> StringId {
self.runtime.intern_string(s)
}
fn register_span(&mut self, range: rnix::TextRange) -> u32 {
let id = self.code.spans.len();
let source_id = self
.code
.sources
.len()
.checked_sub(1)
.expect("current_source not set");
self.code.spans.push((source_id, range));
id as u32
}
fn get_code(&self) -> &[u8] {
&self.code.bytecode
}
fn get_code_mut(&mut self) -> &mut Vec<u8> {
&mut self.code.bytecode
}
fn add_constant(&mut self, val: Const) -> u32 {
use Const::*;
let val = match val {
Smi(x) => StaticValue::new(x),
Float(x) => StaticValue::new(x),
Bool(x) => StaticValue::new(x),
String(x) => StaticValue::new(x),
Path(x) => StaticValue::new(fix_runtime::Path(x)),
PrimOp(id) => StaticValue::new(fix_runtime::PrimOp::from(id)),
Null => StaticValue::default(),
};
self.runtime.add_const(val)
}
fn current_source_dir(&mut self) -> StringId {
let dir = self
.code
.sources
.last()
.expect("current_source not set")
.get_dir()
.to_string_lossy()
.into_owned();
self.runtime.intern_string(dir)
}
}
fn parse_error_span(error: &rnix::ParseError) -> Option<rnix::TextRange> {
use rnix::ParseError::*;
match error {
Unexpected(range)
| UnexpectedExtra(range)
| UnexpectedWanted(_, range, _)
| UnexpectedDoubleBind(range)
| DuplicatedArgs(range, _) => Some(*range),
_ => None,
}
}
fn handle_parse_error<'a>(
errors: impl IntoIterator<Item = &'a rnix::ParseError>,
source: Source,
) -> Option<Box<Error>> {
for err in errors {
if let Some(span) = parse_error_span(err) {
return Some(
Error::parse_error(err.to_string())
.with_source(source)
.with_span(span),
);
}
}
None
}
struct DowngradeCtx<'ctx, 'id, 'ir, R: VmRuntimeCtx> {
bump: &'ir Bump,
token: GhostToken<'id>,
runtime: &'ctx mut R,
source: Source,
scopes: Vec<Scope<'ctx, 'id, 'ir>>,
with_stack: Vec<GhostRoMaybeThunkRef<'id, 'ir>>,
thunk_count: &'ctx mut usize,
thunk_scopes: Vec<ThunkScope<'id, 'ir>>,
}
impl<'ctx, 'id, 'ir, R: VmRuntimeCtx> DowngradeCtx<'ctx, 'id, 'ir, R> {
fn new(
bump: &'ir Bump,
token: GhostToken<'id>,
runtime: &'ctx mut R,
global: &'ctx HashMap<StringId, MaybeThunk>,
extra_scope: Option<Scope<'ctx, 'id, 'ir>>,
thunk_count: &'ctx mut usize,
source: Source,
) -> Self {
Self {
bump,
token,
runtime,
source,
scopes: std::iter::once(Scope::Global(global))
.chain(extra_scope)
.collect(),
thunk_count,
with_stack: Vec::new(),
thunk_scopes: vec![ThunkScope::new_in(bump)],
}
}
}
impl<'ctx: 'ir, 'id, 'ir, R: VmRuntimeCtx> DowngradeContext<'id, 'ir>
for DowngradeCtx<'ctx, 'id, 'ir, R>
{
fn new_expr(&self, expr: Ir<'ir, GhostRoRef<'id, 'ir>>) -> GhostRoIrRef<'id, 'ir> {
self.bump.alloc(GhostCell::new(expr).into())
}
fn maybe_thunk(&mut self, ir: GhostRoIrRef<'id, 'ir>) -> GhostRoMaybeThunkRef<'id, 'ir> {
use MaybeThunk::*;
let expr = (|| {
let expr = match *ir.borrow(&self.token) {
Ir::Builtin(x) => Builtin(x),
Ir::Int(x) => Int(x),
Ir::Float(x) => Float(x),
Ir::Bool(x) => Bool(x),
Ir::Str(x) => Str(x),
Ir::Arg { layer } => Arg { layer },
Ir::Builtins => Builtins,
Ir::Null => Null,
Ir::MaybeThunk(thunk) => return Some(thunk),
_ => return None,
};
Some(self.bump.alloc(GhostCell::new(expr).into()))
})();
if let Some(thunk) = expr {
return thunk;
}
let id = ThunkId(*self.thunk_count);
*self.thunk_count = self.thunk_count.checked_add(1).expect("thunk id overflow");
self.thunk_scopes
.last_mut()
.expect("no active cache scope")
.add_binding(id, ir);
self.bump.alloc(GhostCell::new(Thunk(id)).into())
}
fn intern_string(&mut self, sym: impl AsRef<str>) -> StringId {
self.runtime.intern_string(sym)
}
fn resolve_sym(&self, id: StringId) -> Symbol<'_> {
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(
&mut self,
sym: StringId,
span: rnix::TextRange,
) -> Result<GhostRoMaybeThunkRef<'id, 'ir>> {
for scope in self.scopes.iter().rev() {
match scope {
&Scope::Global(global_scope) => {
if let Some(expr) = global_scope.get(&sym) {
return Ok(expr.into());
}
}
&Scope::Repl(repl_bindings) => {
if repl_bindings.contains(&sym) {
return Ok(self
.bump
.alloc(GhostCell::new(MaybeThunk::ReplBinding(sym)).into()));
}
}
&Scope::ScopedImport { ref keys, slot_id } => {
if keys.contains(&sym) {
return Ok(self.bump.alloc(
GhostCell::new(MaybeThunk::ScopedImportBinding { sym, slot_id }).into(),
));
}
}
Scope::Let(let_scope) => {
if let Some(&expr) = let_scope.get(&sym) {
return Ok(expr.into());
}
}
&Scope::Param {
sym: param_sym,
abs_layer,
} => {
if param_sym == sym {
let layers: u8 =
self.thunk_scopes.len().try_into().expect("scope too deep!");
let layer = layers - abs_layer;
return Ok(self
.bump
.alloc(GhostCell::new(MaybeThunk::Arg { layer }).into()));
}
}
}
}
if !self.with_stack.is_empty() {
let id = ThunkId(*self.thunk_count);
*self.thunk_count = self.thunk_count.checked_add(1).expect("thunk id overflow");
let mut namespaces =
bumpalo::collections::Vec::with_capacity_in(self.with_stack.len(), self.bump);
namespaces.extend(self.with_stack.iter().rev().copied());
let body = self
.bump
.alloc(GhostCell::new(Ir::WithLookup { sym, namespaces }).into());
self.thunk_scopes
.last_mut()
.expect("no active thunk scope")
.add_binding(id, body);
Ok(self
.bump
.alloc(GhostCell::new(MaybeThunk::Thunk(id)).into()))
} else {
Err(Error::downgrade_error(
format!("'{}' not found", self.resolve_sym(sym)),
self.get_current_source(),
span,
))
}
}
fn get_current_source(&self) -> Source {
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>
where
F: FnOnce(
&mut Self,
) -> Result<(
bumpalo::collections::Vec<'ir, GhostRoMaybeThunkRef<'id, 'ir>>,
Ret,
)>,
{
let base = *self.thunk_count;
*self.thunk_count = self
.thunk_count
.checked_add(keys.len())
.expect("thunk id overflow");
let handles = (base..base + keys.len())
.map(|id| {
&*self
.bump
.alloc(GhostCell::new(MaybeThunk::Thunk(ThunkId(id))))
})
.collect::<Vec<_>>();
let scope = keys.iter().copied().zip(handles.iter().copied()).collect();
self.scopes.push(Scope::Let(scope));
let (vals, ret) = { f(self)? };
self.scopes.pop();
assert_eq!(keys.len(), vals.len());
let scope = self.thunk_scopes.last_mut().expect("no active thunk scope");
for (i, (val, handle)) in vals.into_iter().zip(handles).enumerate() {
let thunk = *val.borrow(&self.token);
*handle.borrow_mut(&mut self.token) = thunk;
let id = ThunkId(base + i);
let ir_ref = self
.bump
.alloc(GhostCell::new(Ir::MaybeThunk(handle.into())).into());
scope.add_binding(id, ir_ref);
}
Ok(ret)
}
fn with_param_scope<F, Ret>(&mut self, sym: StringId, f: F) -> Ret
where
F: FnOnce(&mut Self) -> Ret,
{
self.scopes.push(Scope::Param {
sym,
abs_layer: self.thunk_scopes.len().try_into().expect("scope too deep!"),
});
let mut guard = ScopeGuard { ctx: self };
f(guard.as_ctx())
}
fn with_with_scope<F, Ret>(&mut self, namespace: GhostRoMaybeThunkRef<'id, 'ir>, f: F) -> Ret
where
F: FnOnce(&mut Self) -> Ret,
{
self.with_stack.push(namespace);
let ret = f(self);
self.with_stack.pop();
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>(
&mut self,
f: F,
) -> (
Ret,
bumpalo::collections::Vec<'ir, (ThunkId, GhostRoIrRef<'id, 'ir>)>,
)
where
F: FnOnce(&mut Self) -> Ret,
{
if self.thunk_scopes.len() == u8::MAX as usize {
panic!("scope too deep!");
}
self.thunk_scopes.push(ThunkScope::new_in(self.bump));
let ret = f(self);
(
ret,
self.thunk_scopes
.pop()
.expect("no thunk scope left???")
.bindings,
)
}
fn bump(&self) -> &'ir bumpalo::Bump {
self.bump
}
}
impl<'id, 'ir, 'ctx: 'ir, R: VmRuntimeCtx> DowngradeCtx<'ctx, 'id, 'ir, R> {
fn downgrade_toplevel(mut self, root: rnix::ast::Expr) -> Result<RawIrRef<'ir>> {
let body = root.downgrade(&mut self)?;
let thunks = self
.thunk_scopes
.pop()
.expect("no thunk scope left???")
.bindings;
Ok(Ir::freeze(
self.new_expr(Ir::TopLevel { body, thunks }),
self.token,
))
}
}
struct ThunkScope<'id, 'ir> {
bindings: bumpalo::collections::Vec<'ir, (ThunkId, GhostRoIrRef<'id, 'ir>)>,
}
impl<'id, 'ir> ThunkScope<'id, 'ir> {
fn new_in(bump: &'ir Bump) -> Self {
Self {
bindings: bumpalo::collections::Vec::new_in(bump),
}
}
fn add_binding(&mut self, id: ThunkId, ir: GhostRoIrRef<'id, 'ir>) {
self.bindings.push((id, ir));
}
}
enum Scope<'ctx, 'id, 'ir> {
Global(&'ctx HashMap<StringId, MaybeThunk>),
Repl(&'ctx HashSet<StringId>),
ScopedImport {
keys: HashSet<StringId>,
slot_id: u32,
},
Let(HashMap<StringId, GhostMaybeThunkRef<'id, 'ir>>),
Param {
sym: StringId,
abs_layer: u8,
},
}
pub enum ExtraScope<'ctx> {
Repl(&'ctx HashSet<StringId>),
ScopedImport {
keys: HashSet<StringId>,
slot_id: u32,
},
}
impl<'ctx> From<ExtraScope<'ctx>> for Scope<'ctx, '_, '_> {
fn from(value: ExtraScope<'ctx>) -> Self {
use ExtraScope::*;
match value {
ScopedImport { keys, slot_id } => Scope::ScopedImport { keys, slot_id },
Repl(scope) => Scope::Repl(scope),
}
}
}
struct ScopeGuard<'a, 'ctx, 'id, 'ir, R: VmRuntimeCtx> {
ctx: &'a mut DowngradeCtx<'ctx, 'id, 'ir, R>,
}
impl<'a, 'ctx, 'id, 'ir, R: VmRuntimeCtx> Drop for ScopeGuard<'a, 'ctx, 'id, 'ir, R> {
fn drop(&mut self) {
self.ctx.scopes.pop();
}
}
impl<'a, 'ctx, 'id, 'ir, R: VmRuntimeCtx> ScopeGuard<'a, 'ctx, 'id, 'ir, R> {
fn as_ctx(&mut self) -> &mut DowngradeCtx<'ctx, 'id, 'ir, R> {
self.ctx
}
}
mod sealed {
use super::*;
pub struct OwnedIr {
_bump: Bump,
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;
+8 -13
View File
@@ -3,10 +3,8 @@ use std::marker::PhantomData;
use bumpalo::Bump; use bumpalo::Bump;
use bumpalo::collections::Vec; use bumpalo::collections::Vec;
use fix_builtins::{BUILTINS, BuiltinId}; use fix_lang::{BuiltinId, StringId};
use fix_common::StringId;
use ghost_cell::{GhostCell, GhostToken}; use ghost_cell::{GhostCell, GhostToken};
use num_enum::TryFromPrimitive as _;
use rnix::{TextRange, ast}; use rnix::{TextRange, ast};
use string_interner::DefaultStringInterner; use string_interner::DefaultStringInterner;
@@ -92,7 +90,7 @@ pub enum MaybeThunk {
BuiltinConst(StringId), BuiltinConst(StringId),
Builtins, Builtins,
ReplBinding(StringId), ReplBinding(StringId),
ScopedImportBinding(StringId), ScopedImportBinding { slot_id: u32, sym: StringId },
} }
pub trait Ref<'ir> { pub trait Ref<'ir> {
@@ -213,19 +211,17 @@ pub enum Ir<'ir, R: RefExt<'ir> + ?Sized + 'ir> {
}, },
MaybeThunk(R::MaybeThunkRef), MaybeThunk(R::MaybeThunkRef),
ReplBinding(StringId), ReplBinding(StringId),
ScopedImportBinding(StringId), ScopedImportBinding {
sym: StringId,
slot_id: u32,
},
} }
#[repr(transparent)] #[repr(transparent)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ThunkId(pub usize); pub struct ThunkId(pub usize);
#[repr(transparent)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct SpanId(pub u32);
/// 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.
@@ -294,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_primitive(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));
} }
@@ -1,7 +1,6 @@
use bumpalo::collections::{CollectIn, Vec}; use bumpalo::collections::{CollectIn, Vec};
use fix_builtins::BuiltinId;
use fix_common::Symbol;
use fix_error::{Error, Result, Source}; use fix_error::{Error, Result, Source};
use fix_lang::{BuiltinId, Symbol};
use hashbrown::HashSet; use hashbrown::HashSet;
use hashbrown::hash_map::Entry; use hashbrown::hash_map::Entry;
use rnix::TextRange; use rnix::TextRange;
@@ -157,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)
@@ -463,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
@@ -494,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 { (
Some(Param {
required, required,
optional, optional,
ellipsis, ellipsis,
}); }),
inner_body,
body = inner_body; )
}
} }
};
Result::Ok(Ret { param, body }) Ok(Ret { param, body })
}); });
let Ret { param, body } = ret?; let Ret { param, body } = ret?;
@@ -551,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],
@@ -652,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(),
@@ -666,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>,
@@ -1,14 +1,14 @@
use fix_builtins::{BUILTINS, BuiltinId}; use fix_bytecode::{Const, InstructionPtr, Op, OperandType};
use fix_common::StringId; use fix_lang::StringId;
use fix_ir::{Attr, BinOpKind, Ir, MaybeThunk, Param, RawIrRef, ThunkId, UnOpKind};
use hashbrown::HashMap; use hashbrown::HashMap;
use num_enum::TryFromPrimitive;
use rnix::TextRange; use rnix::TextRange;
use string_interner::Symbol as _; use string_interner::Symbol as _;
pub mod disassembler; mod context;
pub mod ir;
pub struct InstructionPtr(pub usize); pub use context::{CodeState, ExtraScope};
pub use fix_bytecode::disassembler;
pub use ir::{Attr, BinOpKind, Ir, MaybeThunk, Param, RawIrRef, ThunkId, UnOpKind};
pub trait BytecodeContext { pub trait BytecodeContext {
fn intern_string(&mut self, s: &str) -> StringId; fn intern_string(&mut self, s: &str) -> StringId;
@@ -17,86 +17,6 @@ pub trait BytecodeContext {
fn get_code_mut(&mut self) -> &mut Vec<u8>; fn get_code_mut(&mut self) -> &mut Vec<u8>;
fn add_constant(&mut self, val: Const) -> u32; fn add_constant(&mut self, val: Const) -> u32;
fn current_source_dir(&mut self) -> StringId; fn current_source_dir(&mut self) -> StringId;
fn current_scope_slot(&self) -> Option<u32>;
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, TryFromPrimitive)]
#[allow(clippy::enum_variant_names)]
pub enum Op {
PushSmi,
PushBigInt,
PushFloat,
PushString,
PushNull,
PushTrue,
PushFalse,
LoadLocal,
LoadOuter,
StoreLocal,
AllocLocals,
MakeThunk,
MakeClosure,
MakePatternClosure,
Call,
DispatchPrimOp,
MakeAttrs,
MakeEmptyAttrs,
SelectStatic,
SelectDynamic,
HasAttrPathStatic,
HasAttrPathDynamic,
HasAttrStatic,
HasAttrDynamic,
HasAttrResolve,
JumpIfSelectSucceeded,
JumpIfSelectFailed,
MakeList,
MakeEmptyList,
OpAdd,
OpSub,
OpMul,
OpDiv,
OpEq,
OpNeq,
OpLt,
OpGt,
OpLeq,
OpGeq,
OpConcat,
OpUpdate,
OpNeg,
OpNot,
JumpIfFalse,
JumpIfTrue,
Jump,
CoerceToString,
ConcatStrings,
ResolvePath,
Assert,
LookupWith,
LoadBuiltins,
LoadBuiltin,
LoadReplBinding,
LoadScopedBinding,
Return,
Illegal,
} }
struct ScopeInfo { struct ScopeInfo {
@@ -109,39 +29,6 @@ struct BytecodeEmitter<'a, Ctx: BytecodeContext> {
scope_stack: Vec<ScopeInfo>, scope_stack: Vec<ScopeInfo>,
} }
#[repr(u8)]
#[derive(Debug, Clone, Copy, TryFromPrimitive)]
pub enum OperandType {
Const,
BigInt,
Local,
BuiltinConst,
Builtins,
ReplBinding,
ScopedImportBinding,
}
pub enum Const {
Smi(i32),
Float(f64),
Bool(bool),
String(StringId),
Path(StringId),
PrimOp {
id: BuiltinId,
arity: u8,
dispatch_ip: u32,
},
Null,
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, TryFromPrimitive)]
pub enum AttrKeyType {
Static,
Dynamic,
}
pub enum InlineOperand { pub enum InlineOperand {
Const(Const), Const(Const),
BigInt(i64), BigInt(i64),
@@ -149,7 +36,7 @@ pub enum InlineOperand {
BuiltinConst(StringId), BuiltinConst(StringId),
Builtins, Builtins,
ReplBinding(StringId), ReplBinding(StringId),
ScopedImportBinding(StringId), ScopedImportBinding { id: StringId, slot_id: u32 },
} }
pub fn compile_bytecode(ir: RawIrRef<'_>, ctx: &mut impl BytecodeContext) -> InstructionPtr { pub fn compile_bytecode(ir: RawIrRef<'_>, ctx: &mut impl BytecodeContext) -> InstructionPtr {
@@ -167,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 {
@@ -188,18 +74,13 @@ 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: id.entry_phase().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),
ScopedImportBinding(id) => InlineOperand::ScopedImportBinding(id), ScopedImportBinding { slot_id, sym: id } => {
InlineOperand::ScopedImportBinding { slot_id, id }
}
} }
} }
@@ -232,13 +113,9 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
self.emit_u8(OperandType::ReplBinding as u8); self.emit_u8(OperandType::ReplBinding as u8);
self.emit_str_id(id); self.emit_str_id(id);
} }
ScopedImportBinding(id) => { ScopedImportBinding { id, slot_id } => {
self.emit_u8(OperandType::ScopedImportBinding as u8); self.emit_u8(OperandType::ScopedImportBinding as u8);
let slot = self self.emit_u32(slot_id);
.ctx
.current_scope_slot()
.expect("ScopedImportBinding outside scoped compilation");
self.emit_u32(slot);
self.emit_str_id(id); self.emit_str_id(id);
} }
} }
@@ -301,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());
} }
@@ -329,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) {
@@ -555,14 +440,10 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
self.emit_op(Op::LoadReplBinding); self.emit_op(Op::LoadReplBinding);
self.emit_str_id(name); self.emit_str_id(name);
} }
&Ir::ScopedImportBinding(name) => { &Ir::ScopedImportBinding { sym, slot_id } => {
self.emit_op(Op::LoadScopedBinding); self.emit_op(Op::LoadScopedBinding);
let slot = self self.emit_u32(slot_id);
.ctx self.emit_str_id(sym);
.current_scope_slot()
.expect("ScopedImportBinding outside scoped compilation");
self.emit_u32(slot);
self.emit_str_id(name);
} }
Ir::WithLookup { sym, namespaces } => { Ir::WithLookup { sym, namespaces } => {
// counter // counter
@@ -629,20 +510,20 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
self.emit_op(Op::LoadReplBinding); self.emit_op(Op::LoadReplBinding);
self.emit_str_id(name); self.emit_str_id(name);
} }
ScopedImportBinding(name) => { ScopedImportBinding { slot_id, sym } => {
self.emit_op(Op::LoadScopedBinding); self.emit_op(Op::LoadScopedBinding);
let slot = self self.emit_u32(slot_id);
.ctx self.emit_str_id(sym);
.current_scope_slot()
.expect("ScopedImportBinding outside scoped compilation");
self.emit_u32(slot);
self.emit_str_id(name);
} }
} }
} }
} }
} }
#[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 {
@@ -775,7 +656,7 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
fn emit_attrset( fn emit_attrset(
&mut self, &mut self,
stcs: &fix_ir::HashMap<'_, StringId, (&MaybeThunk, TextRange)>, stcs: &ir::HashMap<'_, StringId, (&MaybeThunk, TextRange)>,
dyns: &[(RawIrRef<'_>, &MaybeThunk, TextRange)], dyns: &[(RawIrRef<'_>, &MaybeThunk, TextRange)],
) { ) {
if stcs.is_empty() && dyns.is_empty() { if stcs.is_empty() && dyns.is_empty() {
@@ -835,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);
@@ -853,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);
+4 -1
View File
@@ -5,5 +5,8 @@ edition = "2024"
[dependencies] [dependencies]
miette = { version = "7.6", features = ["fancy"] } miette = { version = "7.6", features = ["fancy"] }
thiserror = "2.0"
rnix = { workspace = true } rnix = { workspace = true }
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 {
@@ -1,9 +1,13 @@
[package] [package]
name = "fix-common" name = "fix-lang"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
gc-arena = { workspace = true }
string-interner = { workspace = true }
ere = { workspace = true } ere = { workspace = true }
gc-arena = { workspace = true }
num_enum = { workspace = true }
string-interner = { workspace = true }
[lints]
workspace = true
+163 -5
View File
@@ -4,6 +4,162 @@ use std::fmt::{Debug, Display, Formatter, Result as FmtResult};
use std::ops::{Deref, DerefMut}; use std::ops::{Deref, DerefMut};
use gc_arena::Collect; use gc_arena::Collect;
use num_enum::TryFromPrimitive;
macro_rules! define_builtins {
($(($name:literal, $variant:ident, $arity:expr)),* $(,)?) => {
const BUILTINS: &[(&str, u8)] = &[
$(($name, $arity),)*
];
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, TryFromPrimitive, Collect)]
#[repr(u8)]
#[collect(require_static)]
pub enum BuiltinId {
$($variant,)*
}
impl BuiltinId {
pub const ALL: [Self; BUILTINS.len()] = [$(Self::$variant,)*];
}
};
}
define_builtins! {
("abort", Abort, 1),
("__add", Add, 2),
("__addErrorContext", AddErrorContext, 2),
("__all", All, 2),
("__any", Any, 2),
("__appendContext", AppendContext, 2),
("__attrNames", AttrNames, 1),
("__attrValues", AttrValues, 1),
("baseNameOf", BaseNameOf, 1),
("__bitAnd", BitAnd, 2),
("__bitOr", BitOr, 2),
("__bitXor", BitXor, 2),
("break", Break, 1),
("__catAttrs", CatAttrs, 2),
("__ceil", Ceil, 1),
("__compareVersions", CompareVersions, 2),
("__concatLists", ConcatLists, 1),
("__concatMap", ConcatMap, 2),
("__concatStringsSep", ConcatStringsSep, 2),
("__convertHash", ConvertHash, 1),
("__deepSeq", DeepSeq, 2),
("derivation", Derivation, 1),
("derivationStrict", DerivationStrict, 1),
("dirOf", DirOf, 1),
("__div", Div, 2),
("__elem", Elem, 2),
("__elemAt", ElemAt, 2),
("fetchGit", FetchGit, 1),
("fetchMercurial", FetchMercurial, 1),
("fetchTarball", FetchTarball, 1),
("fetchTree", FetchTree, 1),
("__fetchurl", FetchUrl, 1),
("__filter", Filter, 2),
("__filterSource", FilterSource, 2),
("__findFile", FindFile, 2),
("__floor", Floor, 1),
("__foldl'", FoldlStrict, 3),
("__fromJSON", FromJSON, 1),
("fromTOML", FromTOML, 1),
("__functionArgs", FunctionArgs, 1),
("__genList", GenList, 2),
("__genericClosure", GenericClosure, 1),
("__getAttr", GetAttr, 2),
("__getContext", GetContext, 1),
("__getEnv", GetEnv, 1),
("__groupBy", GroupBy, 2),
("__hasAttr", HasAttr, 2),
("__hasContext", HasContext, 1),
("__hashFile", HashFile, 2),
("__hashString", HashString, 2),
("__head", Head, 1),
("import", Import, 1),
("__intersectAttrs", IntersectAttrs, 2),
("__isAttrs", IsAttrs, 1),
("__isBool", IsBool, 1),
("__isFloat", IsFloat, 1),
("__isFunction", IsFunction, 1),
("__isInt", IsInt, 1),
("__isList", IsList, 1),
("isNull", IsNull, 1),
("__isPath", IsPath, 1),
("__isString", IsString, 1),
("__length", Length, 1),
("__lessThan", LessThan, 2),
("__listToAttrs", ListToAttrs, 1),
("map", Map, 2),
("__mapAttrs", MapAttrs, 2),
("__match", Match, 2),
("__mul", Mul, 2),
("__parseDrvName", ParseDrvName, 1),
("__partition", Partition, 2),
("__path", Path, 1),
("__pathExists", PathExists, 1),
("placeholder", Placeholder, 1),
("__readDir", ReadDir, 1),
("__readFile", ReadFile, 1),
("__readFileType", ReadFileType, 1),
("removeAttrs", RemoveAttrs, 2),
("__replaceStrings", ReplaceStrings, 3),
("scopedImport", ScopedImport, 2),
("__seq", Seq, 2),
("__sort", Sort, 2),
("__split", Split, 2),
("__splitVersion", SplitVersion, 1),
("__storePath", StorePath, 1),
("__stringLength", StringLength, 1),
("__sub", Sub, 2),
("__substring", Substring, 3),
("__tail", Tail, 1),
("throw", Throw, 1),
("__toFile", ToFile, 2),
("__toJSON", ToJSON, 1),
("__toPath", ToPath, 1),
("toString", ToString, 1),
("__toXML", ToXML, 1),
("__trace", Trace, 2),
("__tryEval", TryEval, 1),
("__typeOf", TypeOf, 1),
("__unsafeDiscardStringContext", UnsafeDiscardStringContext, 1),
("__unsafeDiscardOutputDependency", UnsafeDiscardOutputDependency, 1),
("__unsafeGetAttrPos", UnsafeGetAttrPos, 2),
("__warn", Warn, 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)]
@@ -305,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$}")
@@ -329,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()
}
-17
View File
@@ -1,17 +0,0 @@
[package]
name = "fix-primops"
version = "0.1.0"
edition = "2024"
[dependencies]
gc-arena = { workspace = true }
hashbrown = { workspace = true }
num_enum = { workspace = true }
smallvec = { workspace = true }
string-interner = { workspace = true }
fix-abstract-vm = { path = "../fix-abstract-vm" }
fix-builtins = { path = "../fix-builtins" }
fix-codegen = { path = "../fix-codegen" }
fix-common = { path = "../fix-common" }
fix-error = { path = "../fix-error" }
-87
View File
@@ -1,87 +0,0 @@
mod context;
mod control;
mod conv;
mod eq;
mod io;
mod list;
mod path;
pub use context::*;
pub use control::*;
pub use conv::*;
pub use eq::*;
use fix_abstract_vm::{BytecodeReader, Machine, Step, VmRuntimeCtx};
use fix_builtins::PrimOpPhase;
use fix_error::Error;
use gc_arena::Mutation;
pub use io::*;
pub use list::*;
pub use path::*;
#[allow(clippy::too_many_lines)]
pub fn dispatch_primop<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
use PrimOpPhase::*;
let phase_disc = reader.read_u8();
let Ok(phase) = PrimOpPhase::try_from(phase_disc) else {
return m.finish_err(Error::eval_error("invalid primop phase"));
};
match phase {
Abort => abort(m, ctx, reader, mc),
DeepSeq => deep_seq_force_top(m, reader, mc),
DeepSeqPush => deep_seq_push(m, reader, mc),
DeepSeqLoop => deep_seq_loop(m, reader, mc),
Seq => seq(m, reader, mc),
FilterForceList => filter_force_list(m, reader, mc),
FilterCallPred => filter_call_pred(m, reader, mc),
FilterCheck => filter_check(m, reader, mc),
FoldlStrict => foldl_strict_entry(m, reader, mc),
FoldlStrictEmpty => foldl_strict_empty(m, reader, mc),
FoldlStrictCall1 => foldl_strict_call1(m, reader, mc),
FoldlStrictCall2 => foldl_strict_call2(m, reader, mc),
FoldlStrictUpdate => foldl_strict_update(m, reader, mc),
ForceResultShallow => force_result_shallow(m, ctx, reader, mc),
ForceResultShallowPush => force_result_shallow_push(m, ctx, reader, mc),
ForceResultShallowLoop => force_result_shallow_loop(m, reader, mc),
ForceResultDeepFinish => force_result_deep_finish(m, ctx, reader, mc),
EqStep => eq_step(m, reader, mc),
EqForce => eq_force(m, ctx, reader, mc),
CallPattern => call_pattern(m, ctx, reader, mc),
CallFunctor1 => call_functor_1(m, reader, mc),
CallFunctor2 => call_functor_2(m, reader, mc),
Import => import(m, ctx, reader, mc),
ImportFinalize => import_finalize(m, ctx, reader),
ScopedImport => scoped_import(m, ctx, reader, mc),
ScopedImportFinalize => scoped_import_finalize(m, ctx, reader, mc),
PathExists => path_exists(m, ctx, reader, mc),
ToPath => to_path(m, ctx, reader, mc),
IsPath => is_path(m, reader, mc),
ToString => to_string(m, ctx, reader, mc),
TypeOf => type_of(m, ctx, reader, mc),
HasContext => has_context(m, ctx, reader, mc),
GetContext => get_context(m, ctx, reader, mc),
AppendContext => append_context(m, ctx, reader, mc),
AppendContextLoop => append_context_loop(m, ctx, reader, mc),
AppendContextEntryForced => append_context_entry_forced(m, ctx, reader, mc),
AppendContextOutputsForced => append_context_outputs_forced(m, ctx, reader, mc),
AppendContextOutputElementLoop => append_context_output_element_loop(m, ctx, reader, mc),
AppendContextOutputElementForced => append_context_output_element_forced(m, ctx, reader, mc),
UnsafeDiscardStringContext => unsafe_discard_string_context(m, ctx, reader, mc),
UnsafeDiscardOutputDependency => unsafe_discard_output_dependency(m, ctx, reader, mc),
phase => todo!("primop phase {phase:?}"),
}
}
-166
View File
@@ -1,166 +0,0 @@
use fix_abstract_vm::{
BytecodeReader, List, Machine, MachineExt, NixType, Step, StrictValue, Value,
};
use fix_builtins::PrimOpPhase;
use gc_arena::Mutation;
pub fn filter_force_list<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
m.force_slot(0, reader, mc)?;
let list = match m.peek_forced(0).expect_gc::<List>() {
Ok(list) => list,
Err(got) => return m.finish_type_err(NixType::List, got),
};
if list.inner.borrow().is_empty() {
let val = m.pop();
return m.return_from_primop(val, reader);
}
// prepare stack layout: [ pred list idx acc ]
m.push(Value::new_inline(0));
m.push(Value::new_gc(List::new_gc(mc)));
reader.set_pc(PrimOpPhase::FilterCallPred.ip() as usize);
Step::Continue(())
}
pub fn filter_call_pred<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
m.force_slot(3, reader, mc)?;
let pred = m.peek_forced(3);
#[allow(clippy::unwrap_used)]
let idx = m.peek(1).as_inline::<i32>().unwrap();
#[allow(clippy::unwrap_used)]
let elem = m.peek_forced(2).as_gc::<List>().unwrap().inner.borrow()[idx as usize];
m.push(pred.relax());
m.call(reader, mc, elem, PrimOpPhase::FilterCheck.ip() as usize)
}
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
let _ = m.pop(); // list
let _ = m.pop(); // pred
return m.return_from_primop(acc, reader);
}
m.replace(1, Value::new_inline(idx + 1));
reader.set_pc(PrimOpPhase::FilterCallPred.ip() as usize);
Step::Continue(())
}
// foldl' op nul list
//
// Stack layouts across phases:
// Entry: [op, nul, list]
// Empty: [op, nul]
// Call1: [op, list, idx, acc]
// 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(PrimOpPhase::FoldlStrictEmpty.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(PrimOpPhase::FoldlStrictCall1.ip() as usize);
Step::Continue(())
}
pub fn foldl_strict_empty<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let nul = m.force_and_retry::<StrictValue>(reader, mc)?;
let _ = m.pop(); // op
m.return_from_primop(nul.relax(), reader)
}
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, PrimOpPhase::FoldlStrictCall2.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,
PrimOpPhase::FoldlStrictUpdate.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));
reader.set_pc(PrimOpPhase::FoldlStrictCall1.ip() as usize);
Step::Continue(())
}
@@ -1,18 +1,19 @@
[package] [package]
name = "fix-abstract-vm" name = "fix-runtime"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
gc-arena = { workspace = true } gc-arena = { workspace = true }
hashbrown = { workspace = true } hashbrown = { workspace = true }
num_enum = { workspace = true }
smallvec = { workspace = true } smallvec = { workspace = true }
string-interner = { workspace = true }
likely_stable = { workspace = true }
sptr = "0.3" sptr = "0.3"
string-interner = { workspace = true }
fix-builtins = { path = "../fix-builtins" } fix-bytecode = { path = "../fix-bytecode" }
fix-codegen = { path = "../fix-codegen" }
fix-common = { path = "../fix-common" }
fix-error = { path = "../fix-error" } fix-error = { path = "../fix-error" }
fix-lang = { path = "../fix-lang" }
fix-macros = { path = "../fix-macros" }
[lints]
workspace = true
@@ -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"
); );
// 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 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)
}); });
// SAFETY: `val` points to the `Value`'s 8-byte storage, which is valid and
// suitably aligned to hold `P`.
unsafe { val.write(ptr) }; unsafe { val.write(ptr) };
} }
_ => {
#[cfg(target_pointer_width = "32")] compile_error!("unsupported pointer width");
{ }
let _ = (value, ptr);
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" => {
// SAFETY: `Value` is `#[repr(C, align(8))]` and exactly 8 bytes, so it
// is sound to reinterpret its storage as a `[u8; 8]` cast to `P`; the
// pointer was originally written through this same `P` layout by
// `store_ptr`.
let val = (unsafe { value.whole() } as *const [u8; 8]).cast::<P>(); let val = (unsafe { value.whole() } as *const [u8; 8]).cast::<P>();
// SAFETY: `val` points to the `Value`'s 8-byte storage, which is valid
// and suitably aligned to hold `P`.
let ptr = unsafe { val.read() }; let ptr = unsafe { val.read() };
Strict::map_addr(ptr, |addr| addr & 0x0000_FFFF_FFFF_FFFF) Strict::map_addr(ptr, |addr| addr & 0x0000_FFFF_FFFF_FFFF)
} }
_ => {
#[cfg(target_pointer_width = "32")] compile_error!("unsupported pointer width");
{ }
let _ = value;
unimplemented!("32-bit pointer storage not supported");
} }
} }
@@ -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 }
} }
} }
@@ -1,9 +1,9 @@
use fix_common::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")
} }
} }
@@ -1,10 +1,11 @@
use fix_codegen::InstructionPtr; use fix_bytecode::InstructionPtr;
use fix_common::StringId;
use fix_error::Source; use fix_error::Source;
use fix_lang::{self, StringId};
use hashbrown::HashSet; use hashbrown::HashSet;
use crate::{ use crate::{
AttrSet, Closure, ExtraScope, List, NixString, NixType, Null, Path, PrimOp, PrimOpApp, StaticValue, StrictValue, StringContext, Thunk, ThunkState, Value AttrSet, Closure, ExtraScope, List, NixString, NixType, Null, Path, PrimOp, PrimOpApp,
StaticValue, StrictValue, StringContext, Thunk, ThunkState, Value,
}; };
pub trait VmContext { pub trait VmContext {
@@ -38,15 +39,15 @@ pub trait VmRuntimeCtxExt: VmRuntimeCtx {
/// Returns the string context attached to `val`, or `&[]` if `val` is /// Returns the string context attached to `val`, or `&[]` if `val` is
/// either a non-string or a string without context. /// either a non-string or a string without context.
fn get_string_context<'gc>(&self, val: StrictValue<'gc>) -> &'gc StringContext; fn get_string_context<'gc>(&self, val: StrictValue<'gc>) -> &'gc StringContext;
fn convert_value(&self, val: Value) -> fix_common::Value; fn convert_value(&self, val: Value) -> fix_lang::Value;
} }
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())
} }
} }
@@ -55,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)
@@ -66,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())
@@ -76,43 +77,43 @@ 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()
} }
} }
fn convert_value(&self, val: Value) -> fix_common::Value { fn convert_value(&self, val: Value) -> fix_lang::Value {
self.convert_value_with_seen(val, &mut HashSet::new()) self.convert_value_with_seen(val, &mut HashSet::new())
} }
} }
pub(crate) trait ConvertValueWithSeen: VmRuntimeCtx { pub(crate) trait ConvertValueWithSeen: VmRuntimeCtx {
fn convert_value_with_seen(&self, val: Value, seen: &mut HashSet<u64>) -> fix_common::Value; fn convert_value_with_seen(&self, val: Value, seen: &mut HashSet<u64>) -> fix_lang::Value;
} }
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_common::Value { fn convert_value_with_seen(&self, val: Value, seen: &mut HashSet<u64>) -> fix_lang::Value {
use fix_common::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());
@@ -124,10 +125,10 @@ impl<T: VmRuntimeCtx> ConvertValueWithSeen for T {
for &(key, val) in attrs.entries.iter() { for &(key, val) in attrs.entries.iter() {
let key = self.resolve_string(key).to_owned(); let key = self.resolve_string(key).to_owned();
let converted = self.convert_value_with_seen(val, seen); let converted = self.convert_value_with_seen(val, seen);
map.insert(fix_common::Symbol::from(key), converted); map.insert(fix_lang::Symbol::from(key), converted);
} }
Value::AttrSet(fix_common::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());
@@ -142,21 +143,19 @@ impl<T: VmRuntimeCtx> ConvertValueWithSeen for T {
.copied() .copied()
.map(|v| self.convert_value_with_seen(v, seen)) .map(|v| self.convert_value_with_seen(v, seen))
.collect(); .collect();
Value::List(fix_common::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 = fix_builtins::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 = fix_builtins::BUILTINS[app.primop.id as usize].0;
Value::PrimOpApp(name.strip_prefix("__").unwrap_or(name))
} else { } else {
Value::Null Value::Null
} }
@@ -1,20 +1,23 @@
mod boxing; mod boxing;
mod bytecode_reader;
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;
pub use bytecode_reader::*; 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::*;
@@ -1,13 +1,13 @@
use std::ops::ControlFlow; use std::ops::ControlFlow;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use fix_common::StringId;
use fix_error::Error; use fix_error::Error;
use gc_arena::Mutation; use fix_lang::{self, StringId};
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.
@@ -26,11 +26,15 @@ use crate::{
/// - Imports and scope slots (`import_cache_*` / `scope_slot*` / `set_pending_load`) /// - Imports and scope slots (`import_cache_*` / `scope_slot*` / `set_pending_load`)
pub trait Machine<'gc> { pub trait Machine<'gc> {
fn push(&mut self, val: Value<'gc>); fn push(&mut self, val: Value<'gc>);
#[must_use]
fn pop(&mut self) -> Value<'gc>; fn pop(&mut self) -> Value<'gc>;
#[must_use]
fn peek(&self, depth: usize) -> Value<'gc>; fn peek(&self, depth: usize) -> Value<'gc>;
#[must_use]
fn peek_forced(&self, depth: usize) -> StrictValue<'gc>; fn peek_forced(&self, depth: usize) -> StrictValue<'gc>;
fn pop_forced(&mut self) -> StrictValue<'gc>; fn pop_forced(&mut self) -> StrictValue<'gc>;
fn replace(&mut self, depth: usize, val: Value<'gc>); fn replace(&mut self, depth: usize, val: Value<'gc>);
fn drop_n(&mut self, depth: usize);
fn stack_len(&self) -> usize; fn stack_len(&self) -> usize;
fn force_slot_to_pc( fn force_slot_to_pc(
@@ -61,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!()
@@ -87,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 {
@@ -96,7 +109,7 @@ pub trait Machine<'gc> {
cur.borrow().locals[idx as usize] cur.borrow().locals[idx as usize]
} }
fn finish_ok(&mut self, val: fix_common::Value) -> Step; fn finish_ok(&mut self, val: fix_lang::Value) -> Step;
fn finish_err(&mut self, err: Box<Error>) -> Step; fn finish_err(&mut self, err: Box<Error>) -> Step;
fn finish_type_err(&mut self, expected: NixType, got: NixType) -> Step; fn finish_type_err(&mut self, expected: NixType, got: NixType) -> Step;
@@ -105,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(())
}
}
@@ -1,6 +1,7 @@
use fix_bytecode::OperandData;
use gc_arena::{Gc, Mutation}; use gc_arena::{Gc, Mutation};
use crate::{AttrSet, Machine, OperandData, Value}; use crate::{AttrSet, Machine, Value, VmRuntimeCtx};
/// Resolve a decoded operand into a runtime [`Value`]. /// Resolve a decoded operand into a runtime [`Value`].
/// ///
@@ -11,22 +12,20 @@ use crate::{AttrSet, Machine, OperandData, Value};
pub fn resolve_operand<'gc, M: Machine<'gc>>( pub fn resolve_operand<'gc, M: Machine<'gc>>(
op: &OperandData, op: &OperandData,
mc: &Mutation<'gc>, mc: &Mutation<'gc>,
ctx: &impl VmRuntimeCtx,
m: &M, m: &M,
) -> Value<'gc> { ) -> Value<'gc> {
use OperandData::*; use OperandData::*;
match *op { match *op {
Const(sv) => sv.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)
}
}
@@ -1,14 +1,13 @@
use std::ops::ControlFlow; use std::ops::ControlFlow;
use std::path::PathBuf; use std::path::PathBuf;
use fix_common::StringId;
use fix_error::Error; use fix_error::Error;
use fix_lang::StringId;
use gc_arena::{Collect, Gc}; use gc_arena::{Collect, Gc};
use hashbrown::HashSet; use hashbrown::HashSet;
use crate::{GcEnv, StaticValue, 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)]
@@ -87,13 +86,3 @@ pub enum ExtraScope {
slot_id: u32, slot_id: u32,
}, },
} }
pub enum OperandData {
Const(StaticValue),
BigInt(i64),
Local { layer: u8, idx: u32 },
BuiltinConst(StringId),
Builtins,
ReplBinding(StringId),
ScopedImportBinding { slot_id: u32, name: StringId },
}
@@ -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 {
@@ -89,7 +89,7 @@ impl<'a> IntoIterator for &'a mut StringContext {
impl FromIterator<StringContextElem> for StringContext { impl FromIterator<StringContextElem> for StringContext {
fn from_iter<T: IntoIterator<Item = StringContextElem>>(iter: T) -> Self { fn from_iter<T: IntoIterator<Item = StringContextElem>>(iter: T) -> Self {
Self { Self {
data: iter.into_iter().collect() data: iter.into_iter().collect(),
} }
} }
} }
@@ -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();
@@ -1,17 +1,15 @@
#![allow(dead_code)]
use std::cell::RefCell; use std::cell::RefCell;
use std::fmt; use std::fmt;
use std::marker::PhantomData; use std::marker::PhantomData;
use std::mem::size_of; use std::mem::size_of;
use std::ops::Deref; use std::ops::Deref;
use fix_builtins::BuiltinId; use fix_bytecode::Continuation;
use fix_common::*; 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};
use num_enum::TryFromPrimitive;
use smallvec::SmallVec; use smallvec::SmallVec;
use string_interner::Symbol; use string_interner::Symbol;
use string_interner::symbol::SymbolU32; use string_interner::symbol::SymbolU32;
@@ -23,37 +21,99 @@ mod private {
pub trait Cealed {} pub trait Cealed {}
} }
/// # Safety pub trait ValueVariant<'gc>: private::Cealed {
/// #[expect(
/// TAG must be unique among all implementors. private_bounds,
#[allow(private_interfaces)] reason = "Storable is a sealed implementation detail of the value system"
pub unsafe trait Storable: private::Cealed { )]
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;
} }
#[allow(private_bounds)]
pub trait InlineStorable: Storable + RawStore {}
pub 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;)* }
) => { ) => {
$( $(
#[allow(private_interfaces)] impl Storable<'_> for $itype {
unsafe impl Storable for $itype { #[inline(always)]
const TAG: RawTag = $itag; 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(())
}
}
)* )*
$( $(
#[allow(private_interfaces)] impl<'gc> Storable<'gc> for Gc<'gc, unelide_lifetimes!('gc; $gtype)> {
unsafe impl Storable for $gtype { #[inline(always)]
const TAG: RawTag = $gtag; fn is(value: &RawBox) -> bool {
value.tag() == Some($gtag)
} }
impl GcStorable for $gtype {} #[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<'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);
@@ -65,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 {
@@ -78,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"),
} }
} }
@@ -95,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"),
} }
@@ -112,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::N6, "Path"; Path => RawTag::P6, NixType::Path, "Path";
} }
gc { gc {
i64 => RawTag::P6, "BigInt"; i64 => RawTag::P7, NixType::Int, "BigInt";
NixString => RawTag::P7, "String"; NixString => RawTag::N1, NixType::String, "String";
AttrSet<'_> => RawTag::N1, "AttrSet"; AttrSet<'_> => RawTag::N2, NixType::AttrSet, "AttrSet";
List<'_> => RawTag::N2, "List"; List<'_> => RawTag::N3, NixType::List, "List";
Thunk<'_> => RawTag::N3, "Thunk"; Thunk<'_> => RawTag::N4, NixType::Thunk, "Thunk";
Closure<'_> => RawTag::N4, "Closure"; Closure<'_> => RawTag::N5, NixType::Closure, "Closure";
PrimOpApp<'_> => RawTag::N5, "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)
} }
} }
@@ -143,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()
@@ -178,75 +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]
pub fn new_inline<T: InlineStorable>(val: T) -> Self {
Self::from_raw_value(RawValue::store(T::TAG, val))
}
#[inline]
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]
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]
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]
pub fn as_gc<T: GcStorable>(self) -> Option<Gc<'gc, T>> { pub fn is<T: ValueVariant<'gc>>(self) -> bool {
if self.is::<T>() { T::Ty::is(&self.raw)
Some(unsafe {
let rv = self.raw.value().unwrap_unchecked();
let ptr: *const T = <*const T as RawStore>::from_val(rv);
Gc::from_ptr(ptr)
})
} else {
None
} }
#[inline]
pub fn downcast<T: ValueVariant<'gc>>(self) -> Option<T::Ty> {
self.is::<T>()
// SAFETY: `is::<T>()` returned true, so `self.raw` represents a
// valid `T::Ty`.
.then(|| unsafe { T::Ty::from_raw_box(self.raw) })
} }
#[inline] #[inline]
@@ -255,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))
@@ -275,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
@@ -308,28 +359,13 @@ impl<'gc> Value<'gc> {
} }
#[inline] #[inline]
pub fn expect_inline<T: InlineStorable>(self) -> Result<T, NixType> { pub fn expect<T: ValueVariant<'gc>>(self) -> Result<T::Ty, NixType> {
self.as_inline::<T>().ok_or_else(|| self.ty()) self.downcast::<T>().ok_or_else(|| self.ty())
}
#[inline]
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())
} }
} }
@@ -347,37 +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]
pub fn new_inline<T: InlineStorable>(val: T) -> Self { pub fn is<T: ValueVariant<'static>>(self) -> bool {
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]
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]
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()
@@ -476,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)
@@ -487,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::*;
@@ -547,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() }
} }
} }
@@ -570,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 {
@@ -586,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;
@@ -623,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();
@@ -638,7 +680,7 @@ impl RawStore for PrimOp {
fn from_val(value: &RawValue) -> Self { fn from_val(value: &RawValue) -> Self {
let [id, arity, bytes @ ..] = *value.data(); let [id, arity, bytes @ ..] = *value.data();
Self { Self {
id: BuiltinId::try_from_primitive(id).expect("invalid BuiltinId"), id: BuiltinId::try_from(id).expect("invalid BuiltinId"),
arity, arity,
dispatch_ip: u32::from_le_bytes(bytes), dispatch_ip: u32::from_le_bytes(bytes),
} }
@@ -673,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)
+11 -13
View File
@@ -3,22 +3,20 @@ name = "fix-vm"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
[features]
tailcall = []
[dependencies] [dependencies]
gc-arena = { workspace = true } gc-arena = { workspace = true }
hashbrown = { workspace = true } hashbrown = { workspace = true }
num_enum = { workspace = true }
smallvec = { workspace = true } smallvec = { workspace = true }
string-interner = { workspace = true } sysinfo = { version = "0.39", default-features = false, features = ["system"] }
likely_stable = { workspace = true }
sptr = "0.3"
sysinfo = { version = "0.38", default-features = false, features = ["system"] }
fix-builtins = { path = "../fix-builtins" } fix-bytecode = { path = "../fix-bytecode" }
fix-codegen = { path = "../fix-codegen" }
fix-common = { path = "../fix-common" }
fix-error = { path = "../fix-error" } fix-error = { path = "../fix-error" }
fix-abstract-vm = { path = "../fix-abstract-vm" } fix-lang = { path = "../fix-lang" }
fix-primops = { path = "../fix-primops" } fix-macros = { path = "../fix-macros" }
fix-runtime = { path = "../fix-runtime" }
[features]
tailcall = []
[lints]
workspace = true
+17 -12
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>,
@@ -61,7 +62,7 @@ macro_rules! tail_fn {
pc: u32, pc: u32,
fuel: u32, fuel: u32,
) -> TailResult { ) -> TailResult {
let result = vm.$name(); let result = crate::instructions::$name(vm);
tail_dispatch_after!(result, pc + 1, vm, mc, ctx, bc, table, fuel) tail_dispatch_after!(result, pc + 1, vm, mc, ctx, bc, table, fuel)
} }
}; };
@@ -76,7 +77,7 @@ macro_rules! tail_fn {
fuel: u32, fuel: u32,
) -> TailResult { ) -> TailResult {
let mut reader = BytecodeReader::from_after_op(bc, pc as usize); let mut reader = BytecodeReader::from_after_op(bc, pc as usize);
let result = vm.$name(&mut reader); let result = crate::instructions::$name(vm, &mut reader);
tail_dispatch_after!(result, reader.pc() as u32, vm, mc, ctx, bc, table, fuel) tail_dispatch_after!(result, reader.pc() as u32, vm, mc, ctx, bc, table, fuel)
} }
}; };
@@ -91,7 +92,7 @@ macro_rules! tail_fn {
fuel: u32, fuel: u32,
) -> TailResult { ) -> TailResult {
let mut reader = BytecodeReader::from_after_op(bc, pc as usize); let mut reader = BytecodeReader::from_after_op(bc, pc as usize);
let result = vm.$name(&mut reader, mc); let result = crate::instructions::$name(vm, &mut reader, mc);
tail_dispatch_after!(result, reader.pc() as u32, vm, mc, ctx, bc, table, fuel) tail_dispatch_after!(result, reader.pc() as u32, vm, mc, ctx, bc, table, fuel)
} }
}; };
@@ -106,7 +107,7 @@ macro_rules! tail_fn {
fuel: u32, fuel: u32,
) -> TailResult { ) -> TailResult {
let mut reader = BytecodeReader::from_after_op(bc, pc as usize); let mut reader = BytecodeReader::from_after_op(bc, pc as usize);
let result = vm.$name(ctx, &mut reader, mc); let result = crate::instructions::$name(vm, ctx, &mut reader, mc);
tail_dispatch_after!(result, reader.pc() as u32, vm, mc, ctx, bc, table, fuel) tail_dispatch_after!(result, reader.pc() as u32, vm, mc, ctx, bc, table, fuel)
} }
}; };
@@ -120,7 +121,7 @@ macro_rules! tail_fn {
pc: u32, pc: u32,
fuel: u32, fuel: u32,
) -> TailResult { ) -> TailResult {
let result = vm.$name(ctx); let result = crate::instructions::$name(vm, ctx);
tail_dispatch_after!(result, pc + 1, vm, mc, ctx, bc, table, fuel) tail_dispatch_after!(result, pc + 1, vm, mc, ctx, bc, table, fuel)
} }
}; };
@@ -144,7 +145,7 @@ tail_fn!(op_make_closure, (reader, mc));
tail_fn!(op_make_pattern_closure, (reader, mc)); tail_fn!(op_make_pattern_closure, (reader, mc));
tail_fn!(op_call, (ctx, reader, mc)); tail_fn!(op_call, (ctx, reader, mc));
tail_fn!(op_dispatch_primop, (ctx, reader, mc)); tail_fn!(op_dispatch_cont, (ctx, reader, mc));
tail_fn!(op_return, (ctx, reader, mc)); tail_fn!(op_return, (ctx, reader, mc));
tail_fn!(op_make_attrs, (ctx, reader, mc)); tail_fn!(op_make_attrs, (ctx, reader, mc));
@@ -200,18 +201,18 @@ 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_codegen::Op::$variant as usize] = $fn; )* $( arr[fix_bytecode::Op::$variant as usize] = $fn; )*
DispatchTable(arr) DispatchTable(arr)
}; };
} }
// Exhaustiveness check: fails to compile if `fix_codegen::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_codegen::Op) = |op| match op { $( fix_bytecode::Op::$variant => (), )*
$( fix_codegen::Op::$variant => (), )*
}; };
}; };
} }
@@ -235,7 +236,7 @@ table! {
MakePatternClosure => op_make_pattern_closure, MakePatternClosure => op_make_pattern_closure,
Call => op_call, Call => op_call,
DispatchPrimOp => op_dispatch_primop, DispatchCont => op_dispatch_cont,
Return => op_return, Return => op_return,
MakeAttrs => op_make_attrs, MakeAttrs => op_make_attrs,
@@ -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>,
+144 -126
View File
@@ -1,19 +1,18 @@
use std::cmp::Ordering; use std::cmp::Ordering;
use fix_abstract_vm::*; use fix_runtime::*;
use gc_arena::{Gc, Mutation, RefLock}; use gc_arena::{Gc, Mutation, RefLock};
use crate::{BytecodeReader, NixNum, Step, VmError, VmRuntimeCtx}; use crate::{BytecodeReader, NixNum, Step, VmError, VmRuntimeCtx};
impl<'gc> crate::Vm<'gc> { #[inline(always)]
#[inline(always)] pub(crate) fn op_add<'gc, M: Machine<'gc>>(
pub(crate) fn op_add( m: &mut M,
&mut self,
ctx: &mut impl VmRuntimeCtx, ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>, reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>, mc: &Mutation<'gc>,
) -> Step { ) -> Step {
let (lhs, rhs) = self.force_and_retry::<(StrictValue, StrictValue)>(reader, mc)?; let (lhs, rhs) = m.force_and_retry::<(StrictValue, StrictValue)>(reader, mc)?;
// if the LHS is a path, the result is a path obtained by // if the LHS is a path, the result is a path obtained by
// canonicalizing the concatenated string. RHS may be a path or a // canonicalizing the concatenated string. RHS may be a path or a
// string. (A `string + path` keeps the string-typed result, handled // string. (A `string + path` keeps the string-typed result, handled
@@ -21,7 +20,7 @@ impl<'gc> crate::Vm<'gc> {
if lhs.is::<Path>() { if lhs.is::<Path>() {
let (Some(ls), Some(rs)) = (ctx.get_string_or_path(lhs), ctx.get_string_or_path(rhs)) let (Some(ls), Some(rs)) = (ctx.get_string_or_path(lhs), ctx.get_string_or_path(rhs))
else { else {
return self.finish_err(fix_error::Error::eval_error(format!( return m.finish_err(fix_error::Error::eval_error(format!(
"cannot append {} to a path", "cannot append {} to a path",
rhs.ty() rhs.ty()
))); )));
@@ -29,7 +28,7 @@ impl<'gc> crate::Vm<'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);
self.push(Value::new_inline(fix_abstract_vm::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)) {
@@ -40,54 +39,66 @@ impl<'gc> crate::Vm<'gc> {
mc, mc,
crate::NixString::with_context(format!("{ls}{rs}"), merged), crate::NixString::with_context(format!("{ls}{rs}"), merged),
); );
self.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);
match res { match res {
Ok(val) => { Ok(val) => {
self.push(val); m.push(val);
Step::Continue(()) Step::Continue(())
} }
Err(e) => self.finish_vm_err(e), Err(e) => m.finish_vm_err(e),
}
} }
}
#[inline(always)] #[inline(always)]
pub(crate) fn op_sub(&mut self, reader: &mut BytecodeReader<'_>, mc: &Mutation<'gc>) -> Step { pub(crate) fn op_sub<'gc, M: Machine<'gc>>(
self.op_arith(reader, mc, i64::wrapping_sub, |a, b| a - b) m: &mut M,
} reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
op_arith(m, reader, mc, i64::wrapping_sub, |a, b| a - b)
}
#[inline(always)] #[inline(always)]
pub(crate) fn op_mul(&mut self, reader: &mut BytecodeReader<'_>, mc: &Mutation<'gc>) -> Step { pub(crate) fn op_mul<'gc, M: Machine<'gc>>(
self.op_arith(reader, mc, i64::wrapping_mul, |a, b| a * b) m: &mut M,
} reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
op_arith(m, reader, mc, i64::wrapping_mul, |a, b| a * b)
}
#[inline(always)] #[inline(always)]
fn op_arith( fn op_arith<'gc, M: Machine<'gc>>(
&mut self, m: &mut M,
reader: &mut BytecodeReader<'_>, reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>, mc: &Mutation<'gc>,
int_op: fn(i64, i64) -> i64, int_op: fn(i64, i64) -> i64,
float_op: fn(f64, f64) -> f64, float_op: fn(f64, f64) -> f64,
) -> Step { ) -> Step {
let (lhs, rhs) = self.force_and_retry::<(StrictValue, StrictValue)>(reader, mc)?; let (lhs, rhs) = m.force_and_retry::<(StrictValue, StrictValue)>(reader, mc)?;
let res = numeric_binop(lhs, rhs, mc, int_op, float_op); let res = numeric_binop(lhs, rhs, mc, int_op, float_op);
match res { match res {
Ok(val) => { Ok(val) => {
self.push(val); m.push(val);
Step::Continue(()) Step::Continue(())
} }
Err(e) => self.finish_vm_err(e), Err(e) => m.finish_vm_err(e),
}
} }
}
#[inline(always)] #[inline(always)]
pub(crate) fn op_div(&mut self, reader: &mut BytecodeReader<'_>, mc: &Mutation<'gc>) -> Step { pub(crate) fn op_div<'gc, M: Machine<'gc>>(
let (lhs, rhs) = self.force_and_retry::<(StrictValue, StrictValue)>(reader, mc)?; m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let (lhs, rhs) = m.force_and_retry::<(StrictValue, StrictValue)>(reader, mc)?;
match (get_num(lhs), get_num(rhs)) { match (get_num(lhs), get_num(rhs)) {
(_, Some(NixNum::Int(0))) | (_, Some(NixNum::Float(0.))) => { (_, Some(NixNum::Int(0))) | (_, Some(NixNum::Float(0.))) => {
return self.finish_vm_err(VmError::Uncatchable(fix_error::Error::eval_error( return m.finish_vm_err(VmError::Uncatchable(fix_error::Error::eval_error(
"division by zero", "division by zero",
))); )));
} }
@@ -96,143 +107,151 @@ impl<'gc> crate::Vm<'gc> {
let res = numeric_binop(lhs, rhs, mc, |a, b| a / b, |a, b| a / b); let res = numeric_binop(lhs, rhs, mc, |a, b| a / b, |a, b| a / b);
match res { match res {
Ok(val) => { Ok(val) => {
self.push(val); m.push(val);
Step::Continue(()) Step::Continue(())
} }
Err(e) => self.finish_vm_err(e), Err(e) => m.finish_vm_err(e),
}
} }
}
#[inline(always)] #[inline(always)]
pub(crate) fn op_eq( pub(crate) fn op_eq<'gc, M: Machine<'gc>>(
&mut self, 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 {
let (lhs, rhs) = self.force_and_retry::<(StrictValue, StrictValue)>(reader, mc)?; let (lhs, rhs) = m.force_and_retry::<(StrictValue, StrictValue)>(reader, mc)?;
fix_primops::start_eq(self, ctx, reader, mc, lhs, rhs, false) crate::primops::start_eq(m, ctx, reader, mc, lhs, rhs, false)
} }
#[inline(always)] #[inline(always)]
pub(crate) fn op_neq( pub(crate) fn op_neq<'gc, M: Machine<'gc>>(
&mut self, 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 {
let (lhs, rhs) = self.force_and_retry::<(StrictValue, StrictValue)>(reader, mc)?; let (lhs, rhs) = m.force_and_retry::<(StrictValue, StrictValue)>(reader, mc)?;
fix_primops::start_eq(self, ctx, reader, mc, lhs, rhs, true) crate::primops::start_eq(m, ctx, reader, mc, lhs, rhs, true)
} }
#[inline(always)] #[inline(always)]
pub(crate) fn op_lt( pub(crate) fn op_lt<'gc, M: Machine<'gc>>(
&mut self, 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 {
self.compare_values(ctx, reader, mc, Ordering::is_lt) compare_values(m, ctx, reader, mc, Ordering::is_lt)
} }
#[inline(always)] #[inline(always)]
pub(crate) fn op_gt( pub(crate) fn op_gt<'gc, M: Machine<'gc>>(
&mut self, 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 {
self.compare_values(ctx, reader, mc, Ordering::is_gt) compare_values(m, ctx, reader, mc, Ordering::is_gt)
} }
#[inline(always)] #[inline(always)]
pub(crate) fn op_leq( pub(crate) fn op_leq<'gc, M: Machine<'gc>>(
&mut self, 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 {
self.compare_values(ctx, reader, mc, Ordering::is_le) compare_values(m, ctx, reader, mc, Ordering::is_le)
} }
#[inline(always)] #[inline(always)]
pub(crate) fn op_geq( pub(crate) fn op_geq<'gc, M: Machine<'gc>>(
&mut self, 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 {
self.compare_values(ctx, reader, mc, Ordering::is_ge) compare_values(m, ctx, reader, mc, Ordering::is_ge)
} }
fn compare_values( fn compare_values<'gc, M: Machine<'gc>>(
&mut self, m: &mut M,
ctx: &impl VmRuntimeCtx, ctx: &impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>, reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>, mc: &Mutation<'gc>,
pred: fn(Ordering) -> bool, pred: fn(Ordering) -> bool,
) -> Step { ) -> Step {
let (lhs, rhs) = self.force_and_retry::<(StrictValue, StrictValue)>(reader, mc)?; let (lhs, rhs) = m.force_and_retry::<(StrictValue, StrictValue)>(reader, mc)?;
match self.compare_values_inner(ctx, pred, lhs, rhs) { match compare_values_inner(m, ctx, pred, lhs, rhs) {
Ok(()) => Step::Continue(()), Ok(()) => Step::Continue(()),
Err(e) => self.finish_vm_err(e), Err(e) => m.finish_vm_err(e),
}
} }
}
#[inline(always)] #[inline(always)]
pub(crate) fn op_concat( pub(crate) fn op_concat<'gc, M: Machine<'gc>>(
&mut self, m: &mut M,
reader: &mut BytecodeReader<'_>, reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>, mc: &Mutation<'gc>,
) -> Step { ) -> Step {
let (l, r) = self.force_and_retry::<(Gc<List>, Gc<List>)>(reader, mc)?; let (l, r) = m.force_and_retry::<(Gc<List>, Gc<List>)>(reader, mc)?;
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());
self.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),
}, },
))); )));
Step::Continue(()) Step::Continue(())
} }
#[inline(always)] #[inline(always)]
pub(crate) fn op_update( pub(crate) fn op_update<'gc, M: Machine<'gc>>(
&mut self, m: &mut M,
reader: &mut BytecodeReader<'_>, reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>, mc: &Mutation<'gc>,
) -> Step { ) -> Step {
let (l, r) = self.force_and_retry::<(Gc<AttrSet>, Gc<AttrSet>)>(reader, mc)?; let (l, r) = m.force_and_retry::<(Gc<AttrSet>, Gc<AttrSet>)>(reader, mc)?;
self.push(Value::new_gc(l.merge(&r, mc))); m.push(Value::new(l.merge(&r, mc)));
Step::Continue(()) Step::Continue(())
} }
#[inline(always)] #[inline(always)]
pub(crate) fn op_neg(&mut self, reader: &mut BytecodeReader<'_>, mc: &Mutation<'gc>) -> Step { pub(crate) fn op_neg<'gc, M: Machine<'gc>>(
let rhs = self.force_and_retry::<NixNum>(reader, mc)?; m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let rhs = m.force_and_retry::<NixNum>(reader, mc)?;
match rhs { match rhs {
NixNum::Int(int) => self.push(Value::make_int(-int, mc)), NixNum::Int(int) => m.push(Value::make_int(-int, mc)),
NixNum::Float(float) => self.push(Value::new_float(-float)), NixNum::Float(float) => m.push(Value::new(-float)),
} }
Step::Continue(()) Step::Continue(())
} }
#[inline(always)] #[inline(always)]
pub(crate) fn op_not(&mut self, reader: &mut BytecodeReader<'_>, mc: &Mutation<'gc>) -> Step { pub(crate) fn op_not<'gc, M: Machine<'gc>>(
let rhs = self.force_and_retry::<bool>(reader, mc)?; m: &mut M,
self.push(Value::new_inline(!rhs)); reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let rhs = m.force_and_retry::<bool>(reader, mc)?;
m.push(Value::new(!rhs));
Step::Continue(()) Step::Continue(())
} }
fn compare_values_inner( fn compare_values_inner<'gc, M: Machine<'gc>>(
&mut self, m: &mut M,
ctx: &impl VmRuntimeCtx, ctx: &impl VmRuntimeCtx,
pred: fn(Ordering) -> bool, pred: fn(Ordering) -> bool,
lhs: StrictValue<'gc>, lhs: StrictValue<'gc>,
rhs: StrictValue<'gc>, rhs: StrictValue<'gc>,
) -> crate::VmResult<()> { ) -> crate::VmResult<()> {
if let (Some(a), Some(b)) = (get_num(lhs), get_num(rhs)) { if let (Some(a), Some(b)) = (get_num(lhs), get_num(rhs)) {
let ord = match (a, b) { let ord = match (a, b) {
(NixNum::Int(a), NixNum::Int(b)) => a.cmp(&b), (NixNum::Int(a), NixNum::Int(b)) => a.cmp(&b),
@@ -244,31 +263,34 @@ impl<'gc> crate::Vm<'gc> {
a.partial_cmp(&(b as f64)).unwrap_or(Ordering::Less) a.partial_cmp(&(b as f64)).unwrap_or(Ordering::Less)
} }
}; };
self.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)) {
self.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);
self.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
Err(crate::vm_err("cannot compare these types")) Err(crate::vm_err(format!(
} "cannot compare {} with {}",
lhs.ty(),
rhs.ty()
)))
} }
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(i as i64)) 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)
} }
} }
@@ -282,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())
+88 -76
View File
@@ -1,6 +1,6 @@
use fix_abstract_vm::{resolve_operand, *}; use fix_bytecode::Continuation;
use fix_builtins::PrimOpPhase;
use fix_error::Error; use fix_error::Error;
use fix_runtime::{resolve_operand, *};
use gc_arena::{Gc, Mutation, RefLock}; use gc_arena::{Gc, Mutation, RefLock};
use crate::{ use crate::{
@@ -8,31 +8,35 @@ use crate::{
VmRuntimeCtxExt, VmRuntimeCtxExt,
}; };
impl<'gc> crate::Vm<'gc> { #[inline(always)]
#[inline(always)] #[expect(
pub(crate) fn call( clippy::indexing_slicing,
&mut self, 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>>(
m: &mut M,
reader: &mut BytecodeReader<'_>, reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>, mc: &Mutation<'gc>,
arg: Value<'gc>, arg: Value<'gc>,
resume_pc: usize, resume_pc: usize,
) -> Step { ) -> Step {
let func = self.force_and_retry::<StrictValue>(reader, mc)?; let func = m.force_and_retry::<StrictValue>(reader, mc)?;
if self.call_depth > 10000 { if m.call_depth() > 10000 {
return self.finish_err(Error::eval_error("stack overflow; max-call-depth exceeded")); return m.finish_err(Error::eval_error("stack overflow; max-call-depth exceeded"));
} }
self.call_depth += 1; 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...
self.push(func.relax()); m.push(func.relax());
self.push(arg); m.push(arg);
self.call_stack.push(CallFrame { m.push_call_frame(CallFrame {
pc: resume_pc, pc: resume_pc,
thunk: None, thunk: None,
env: self.env, env: m.env(),
depth: None,
}); });
reader.set_pc(PrimOpPhase::CallPattern.ip() as usize); reader.set_pc(Continuation::CallPattern.ip() as usize);
return Step::Continue(()); return Step::Continue(());
} }
@@ -40,20 +44,22 @@ impl<'gc> crate::Vm<'gc> {
let n_locals = closure.n_locals; let n_locals = closure.n_locals;
let env = closure.env; let env = closure.env;
let new_env = Gc::new(mc, RefLock::new(Env::with_arg(arg, n_locals, env))); let new_env = Gc::new(mc, RefLock::new(Env::with_arg(arg, n_locals, env)));
self.call_stack.push(CallFrame { m.push_call_frame(CallFrame {
pc: resume_pc, pc: resume_pc,
thunk: None, thunk: None,
env: self.env, env: m.env(),
depth: None,
}); });
reader.set_pc(ip as usize); reader.set_pc(ip as usize);
self.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 {
self.push(arg); m.push(arg);
self.call_stack.push(CallFrame { m.push_call_frame(CallFrame {
pc: resume_pc, pc: resume_pc,
thunk: None, thunk: None,
env: self.env, env: m.env(),
depth: None,
}); });
reader.set_pc(primop.dispatch_ip as usize) reader.set_pc(primop.dispatch_ip as usize)
} else { } else {
@@ -62,18 +68,19 @@ impl<'gc> crate::Vm<'gc> {
arity: primop.arity - 1, arity: primop.arity - 1,
args: [arg, Value::default(), Value::default()], args: [arg, Value::default(), Value::default()],
}; };
self.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 {
self.push(app.args[i as usize]); m.push(app.args[i as usize]);
} }
self.push(arg); m.push(arg);
self.call_stack.push(CallFrame { m.push_call_frame(CallFrame {
pc: resume_pc, pc: resume_pc,
thunk: None, thunk: None,
env: self.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 {
@@ -83,79 +90,82 @@ impl<'gc> crate::Vm<'gc> {
..*app ..*app
}; };
new_app.args[position] = arg; new_app.args[position] = arg;
self.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(self.functor_sym) && let Some(functor) = attrs.lookup(m.functor_sym())
{ {
// f arg => (f.__functor f) arg // f arg => (f.__functor f) arg
// //
// Stage the work for `CallFunctor1` so retries during force are // Stage the work for `CallFunctor1` so retries during force are
// safe: the stack invariant `[..., orig_arg, self, functor]` // safe: the stack invariant `[..., orig_arg, self, functor]`
// holds every time control re-enters phase 1. // holds every time control re-enters phase 1.
self.call_depth -= 1; m.dec_call_depth();
self.call_stack.push(CallFrame { m.push_call_frame(CallFrame {
pc: resume_pc, pc: resume_pc,
thunk: None, thunk: None,
env: self.env, env: m.env(),
depth: None,
}); });
self.push(arg); m.push(arg);
self.push(func.relax()); m.push(func.relax());
self.push(functor); m.push(functor);
reader.set_pc(PrimOpPhase::CallFunctor1.ip() as usize); reader.set_pc(Continuation::CallFunctor1.ip() as usize);
return Step::Continue(()); return Step::Continue(());
} else { } else {
return self.finish_err(Error::eval_error(format!( return m.finish_err(Error::eval_error(format!(
"attempt to call something which is not a function but {}", "attempt to call something which is not a function but {}",
func.ty() func.ty()
))); )));
} }
Step::Continue(()) Step::Continue(())
} }
#[inline(always)] #[inline(always)]
pub(crate) fn op_call( pub(crate) fn op_call<'gc, M: Machine<'gc>>(
&mut self, m: &mut M,
ctx: &impl VmRuntimeCtx, ctx: &impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>, reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>, mc: &Mutation<'gc>,
) -> Step { ) -> Step {
let arg = resolve_operand(&reader.read_operand_data(ctx), mc, self); let arg = resolve_operand(&reader.read_operand_data(), mc, ctx, m);
let pc = reader.pc(); let pc = reader.pc();
self.call(reader, mc, arg, pc) m.call(reader, mc, arg, pc)
} }
#[inline(always)] #[inline(always)]
pub(crate) fn op_return( pub(crate) fn op_return<'gc, M: Machine<'gc>>(
&mut self, 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 {
let val = self.force_and_retry::<StrictValue>(reader, mc)?; let val = m.force_and_retry::<StrictValue>(reader, mc)?;
let Some(CallFrame { let Some(CallFrame {
pc: ret_pc, pc: ret_pc,
thunk, thunk,
env, env,
}) = self.call_stack.pop() depth,
}) = m.pop_call_frame()
else { else {
match self.force_mode { match m.force_mode() {
ForceMode::AsIs => return self.finish_ok(ctx.convert_value(val.relax())), ForceMode::AsIs => return m.finish_ok(ctx.convert_value(val.relax())),
ForceMode::Shallow => { ForceMode::Shallow => {
self.push(val.relax()); m.push(val.relax());
reader.set_pc(PrimOpPhase::ForceResultShallow.ip() as usize); reader.set_pc(Continuation::ForceResultShallow.ip() as usize);
return Step::Continue(()); return Step::Continue(());
} }
ForceMode::Deep => { ForceMode::Deep => {
self.push(val.relax()); m.push(val.relax());
self.push(val.relax()); m.push(val.relax());
self.call_stack.push(CallFrame { m.push_call_frame(CallFrame {
pc: PrimOpPhase::ForceResultDeepFinish.ip() as usize, pc: Continuation::ForceResultDeepFinish.ip() as usize,
thunk: None, thunk: None,
env: self.env, env: m.env(),
depth: None,
}); });
self.call_depth += 1; m.inc_call_depth();
reader.set_pc(PrimOpPhase::DeepSeq.ip() as usize); reader.set_pc(Continuation::PDeepSeq0.ip() as usize);
return Step::Continue(()); return Step::Continue(());
} }
} }
@@ -163,21 +173,23 @@ impl<'gc> crate::Vm<'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 {
self.call_depth -= 1; m.dec_call_depth();
self.push(val.relax()) m.push(val.relax())
} }
self.env = env; m.set_env(env);
Step::Continue(()) Step::Continue(())
} }
#[inline(always)] #[inline(always)]
pub(crate) fn op_dispatch_primop( pub(crate) fn op_dispatch_cont<'gc, M: Machine<'gc>>(
&mut self, 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 {
fix_primops::dispatch_primop(self, ctx, reader, mc) crate::primops::dispatch_cont(m, ctx, reader, mc)
}
} }
+21 -22
View File
@@ -1,32 +1,32 @@
use fix_runtime::Machine;
use gc_arena::{Gc, Mutation, RefLock}; use gc_arena::{Gc, Mutation, RefLock};
use crate::{BytecodeReader, Step, ThunkState, Value}; use crate::{BytecodeReader, Step, ThunkState, Value};
impl<'gc> crate::Vm<'gc> { #[inline(always)]
#[inline(always)] pub(crate) fn op_make_thunk<'gc, M: Machine<'gc>>(
pub(crate) fn op_make_thunk( m: &mut M,
&mut self,
reader: &mut BytecodeReader<'_>, reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>, mc: &Mutation<'gc>,
) -> Step { ) -> Step {
let entry_point = reader.read_u32(); let entry_point = reader.read_u32();
let thunk = Gc::new( let thunk = Gc::new(
mc, mc,
RefLock::new(ThunkState::Pending { RefLock::new(ThunkState::Pending {
ip: entry_point as usize, ip: entry_point as usize,
env: self.env, env: m.env(),
}), }),
); );
self.push(Value::new_gc(thunk)); m.push(Value::new(thunk));
Step::Continue(()) Step::Continue(())
} }
#[inline(always)] #[inline(always)]
pub(crate) fn op_make_closure( pub(crate) fn op_make_closure<'gc, M: Machine<'gc>>(
&mut self, m: &mut M,
reader: &mut BytecodeReader<'_>, reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>, mc: &Mutation<'gc>,
) -> Step { ) -> Step {
let entry_point = reader.read_u32(); let entry_point = reader.read_u32();
let n_locals = reader.read_u32(); let n_locals = reader.read_u32();
let closure = Gc::new( let closure = Gc::new(
@@ -34,20 +34,20 @@ impl<'gc> crate::Vm<'gc> {
crate::Closure { crate::Closure {
ip: entry_point, ip: entry_point,
n_locals, n_locals,
env: self.env, env: m.env(),
pattern: None, pattern: None,
}, },
); );
self.push(Value::new_gc(closure)); m.push(Value::new(closure));
Step::Continue(()) Step::Continue(())
} }
#[inline(always)] #[inline(always)]
pub(crate) fn op_make_pattern_closure( pub(crate) fn op_make_pattern_closure<'gc, M: Machine<'gc>>(
&mut self, m: &mut M,
reader: &mut BytecodeReader<'_>, reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>, mc: &Mutation<'gc>,
) -> Step { ) -> Step {
let entry_point = reader.read_u32(); let entry_point = reader.read_u32();
let n_locals = reader.read_u32(); let n_locals = reader.read_u32();
let req_count = reader.read_u16() as usize; let req_count = reader.read_u16() as usize;
@@ -84,11 +84,10 @@ impl<'gc> crate::Vm<'gc> {
crate::Closure { crate::Closure {
ip: entry_point, ip: entry_point,
n_locals, n_locals,
env: self.env, env: m.env(),
pattern: Some(pattern), pattern: Some(pattern),
}, },
); );
self.push(Value::new_gc(closure)); m.push(Value::new(closure));
Step::Continue(()) Step::Continue(())
}
} }
+135 -111
View File
@@ -1,6 +1,6 @@
use fix_abstract_vm::{NixType, resolve_operand};
use fix_common::StringId;
use fix_error::Error; use fix_error::Error;
use fix_lang::StringId;
use fix_runtime::{Machine, MachineExt, NixType, resolve_operand};
use gc_arena::{Gc, RefLock}; use gc_arena::{Gc, RefLock};
use smallvec::SmallVec; use smallvec::SmallVec;
@@ -8,48 +8,47 @@ use crate::{
AttrSet, BytecodeReader, List, Step, StrictValue, Value, VmRuntimeCtx, VmRuntimeCtxExt, AttrSet, BytecodeReader, List, Step, StrictValue, Value, VmRuntimeCtx, VmRuntimeCtxExt,
}; };
impl<'gc> crate::Vm<'gc> { #[inline(always)]
#[inline(always)] pub(crate) fn op_make_attrs<'gc, M: Machine<'gc>>(
pub(crate) fn op_make_attrs( m: &mut M,
&mut self,
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 {
let static_count = reader.read_u32() as usize; let static_count = reader.read_u32() as usize;
let dynamic_count = reader.read_u32() as usize; let dynamic_count = reader.read_u32() as usize;
for i in 0..dynamic_count { for i in 0..dynamic_count {
let depth = dynamic_count - 1 - i; let depth = dynamic_count - 1 - i;
self.force_slot_to_pc(depth, reader, mc, reader.inst_start_pc())?; m.force_slot_to_pc(depth, reader, mc, reader.inst_start_pc())?;
} }
let mut dyn_keys: SmallVec<[_; 2]> = SmallVec::with_capacity(dynamic_count); let mut dyn_keys: SmallVec<[_; 2]> = SmallVec::with_capacity(dynamic_count);
for i in 0..dynamic_count { for i in 0..dynamic_count {
let depth = dynamic_count - 1 - i; let depth = dynamic_count - 1 - i;
let key_val = self.peek_forced(depth); let key_val = m.peek_forced(depth);
let key_sid = match ctx.get_string_id(key_val) { let key_sid = match ctx.get_string_id(key_val) {
Ok(id) => Some(id), Ok(id) => Some(id),
Err(NixType::Null) => None, Err(NixType::Null) => None,
Err(got) => return self.finish_type_err(NixType::String, got), Err(got) => return m.finish_type_err(NixType::String, got),
}; };
dyn_keys.push(key_sid); dyn_keys.push(key_sid);
} }
self.stack.truncate(self.stack.len() - dynamic_count); m.drop_n(dynamic_count);
let mut kv: SmallVec<[(crate::StringId, Value); 4]> = let mut kv: SmallVec<[(crate::StringId, Value); 4]> =
SmallVec::with_capacity(static_count + dynamic_count); SmallVec::with_capacity(static_count + dynamic_count);
for _ in 0..static_count { for _ in 0..static_count {
let key = reader.read_string_id(); let key = reader.read_string_id();
let val = resolve_operand(&reader.read_operand_data(ctx), mc, self); let val = resolve_operand(&reader.read_operand_data(), mc, ctx, m);
let _span_id = reader.read_u32(); let _span_id = reader.read_u32();
kv.push((key, val)); kv.push((key, val));
} }
for key in dyn_keys { for key in dyn_keys {
let val = resolve_operand(&reader.read_operand_data(ctx), mc, self); let val = resolve_operand(&reader.read_operand_data(), mc, ctx, m);
let _span_id = reader.read_u32(); let _span_id = reader.read_u32();
if let Some(key) = key { if let Some(key) = key {
kv.push((key, val)) kv.push((key, val))
@@ -58,75 +57,80 @@ impl<'gc> crate::Vm<'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));
self.push(Value::new_gc(attrs)); m.push(Value::new(attrs));
Step::Continue(()) Step::Continue(())
} }
#[inline(always)] #[inline(always)]
pub(crate) fn op_make_empty_attrs(&mut self) -> Step { pub(crate) fn op_make_empty_attrs<'gc, M: Machine<'gc>>(m: &mut M) -> Step {
self.push(self.empty_attrs); m.push(m.empty_attrs());
Step::Continue(()) Step::Continue(())
} }
#[inline(always)] #[inline(always)]
pub(crate) fn op_select_static( pub(crate) fn op_select_static<'gc, M: Machine<'gc>>(
&mut self, 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 {
let _span_id = reader.read_u32(); let _span_id = reader.read_u32();
let key = reader.read_string_id(); let key = reader.read_string_id();
let attrset = self.force_and_retry::<Gc<AttrSet>>(reader, mc)?; let attrset = m.force_and_retry::<Gc<AttrSet>>(reader, mc)?;
match attrset.lookup(key) { match attrset.lookup(key) {
Some(v) => { Some(v) => {
self.push(v); m.push(v);
} }
None => return self.select_skip(key, ctx, reader), None => return select_skip(m, key, ctx, reader),
} }
Step::Continue(()) Step::Continue(())
} }
#[inline(always)] #[inline(always)]
pub(crate) fn op_select_dynamic( pub(crate) fn op_select_dynamic<'gc, M: Machine<'gc>>(
&mut self, 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 {
let _span_id = reader.read_u32(); let _span_id = reader.read_u32();
let (attrset, key_val) = self.force_and_retry::<(Gc<AttrSet>, StrictValue)>(reader, mc)?; let (attrset, key_val) = m.force_and_retry::<(Gc<AttrSet>, StrictValue)>(reader, mc)?;
let key_sid = match ctx.get_string_id(key_val) { let key_sid = match ctx.get_string_id(key_val) {
Ok(id) => id, Ok(id) => id,
Err(got) => return self.finish_type_err(NixType::String, got), Err(got) => return m.finish_type_err(NixType::String, got),
}; };
match attrset.lookup(key_sid) { match attrset.lookup(key_sid) {
Some(v) => { Some(v) => {
self.push(v); m.push(v);
} }
None => return self.select_skip(key_sid, ctx, reader), None => return select_skip(m, key_sid, ctx, reader),
} }
Step::Continue(()) Step::Continue(())
} }
/// Skip the rest of a **Select** attrpath after a missing attribute. /// Skip the rest of a **Select** attrpath after a missing attribute.
/// 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.
fn select_skip( #[expect(
&mut self, clippy::cast_sign_loss,
reason = "jump target = pc + offset is a non-negative bytecode address by codegen invariant"
)]
fn select_skip<'gc, M: Machine<'gc>>(
m: &mut M,
key: StringId, key: StringId,
ctx: &mut impl VmRuntimeCtx, ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>, reader: &mut BytecodeReader<'_>,
) -> Step { ) -> Step {
use fix_codegen::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);
} }
@@ -137,23 +141,34 @@ impl<'gc> crate::Vm<'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 self return m.finish_err(Error::eval_error(format!("attribute '{name}' missing")));
.finish_err(Error::eval_error(format!("attribute '{name}' missing")));
}
} }
} }
} }
}
/// 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.
fn has_attr_skip(&mut self, reader: &mut BytecodeReader<'_>) -> Step { #[expect(
use fix_codegen::Op::*; clippy::cast_sign_loss,
reason = "jump target = pc + offset is a non-negative bytecode address by codegen invariant"
)]
#[expect(
clippy::unreachable,
reason = "has_attr_skip only runs over a codegen-produced HasAttr attrpath sequence; any other opcode is a codegen bug"
)]
fn has_attr_skip(reader: &mut BytecodeReader<'_>) -> Step {
use fix_bytecode::Op::*;
loop { loop {
match reader.read_op() { match reader.read_op() {
HasAttrPathStatic => { HasAttrPathStatic => {
@@ -182,86 +197,96 @@ impl<'gc> crate::Vm<'gc> {
} }
} }
} }
} }
#[inline(always)] #[inline(always)]
pub(crate) fn op_has_attr_path_static( pub(crate) fn op_has_attr_path_static<'gc, M: Machine<'gc>>(
&mut self, 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 {
let _span_id = reader.read_u32(); let _span_id = reader.read_u32();
let key = reader.read_string_id(); let key = reader.read_string_id();
let current = self.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) => {
self.push(v); m.push(v);
} }
None => return self.has_attr_skip(reader), None => return has_attr_skip(reader),
} }
Step::Continue(()) Step::Continue(())
} }
#[inline(always)] #[inline(always)]
pub(crate) fn op_has_attr_path_dynamic( pub(crate) fn op_has_attr_path_dynamic<'gc, M: Machine<'gc>>(
&mut self, 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 {
let _span_id = reader.read_u32(); let _span_id = reader.read_u32();
let (current, key_val) = self.force_and_retry::<(StrictValue, StrictValue)>(reader, mc)?; let (current, key_val) = m.force_and_retry::<(StrictValue, StrictValue)>(reader, mc)?;
let key_sid = match ctx.get_string_id(key_val) { let key_sid = match ctx.get_string_id(key_val) {
Ok(id) => id, Ok(id) => id,
Err(got) => return self.finish_type_err(NixType::String, got), Err(got) => return m.finish_type_err(NixType::String, got),
}; };
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) => {
self.push(v); m.push(v);
} }
None => return self.has_attr_skip(reader), None => return has_attr_skip(reader),
} }
Step::Continue(()) Step::Continue(())
} }
#[inline(always)] #[inline(always)]
pub(crate) fn op_jump_if_select_failed(&mut self, reader: &mut BytecodeReader<'_>) -> Step { pub(crate) fn op_jump_if_select_failed<'gc, M: Machine<'gc>>(
_m: &mut M,
reader: &mut BytecodeReader<'_>,
) -> Step {
// No-op // No-op
let _offset = reader.read_i32(); let _offset = reader.read_i32();
Step::Continue(()) Step::Continue(())
} }
#[inline(always)] #[inline(always)]
pub(crate) fn op_jump_if_select_succeeded(&mut self, reader: &mut BytecodeReader<'_>) -> Step { #[expect(
clippy::cast_sign_loss,
reason = "jump target = pc + offset is a non-negative bytecode address by codegen invariant"
)]
pub(crate) fn op_jump_if_select_succeeded<'gc, M: Machine<'gc>>(
_m: &mut M,
reader: &mut BytecodeReader<'_>,
) -> Step {
let offset = reader.read_i32(); 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);
Step::Continue(()) Step::Continue(())
} }
#[inline(always)] #[inline(always)]
pub(crate) fn op_has_attr_static( pub(crate) fn op_has_attr_static<'gc, M: Machine<'gc>>(
&mut self, m: &mut M,
reader: &mut BytecodeReader<'_>, reader: &mut BytecodeReader<'_>,
mc: &gc_arena::Mutation<'gc>, mc: &gc_arena::Mutation<'gc>,
) -> Step { ) -> Step {
let key = reader.read_string_id(); let key = reader.read_string_id();
let current = self.force_and_retry::<StrictValue>(reader, mc)?; let current = m.force_and_retry::<StrictValue>(reader, mc)?;
self.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(),
)); ));
@@ -269,25 +294,25 @@ impl<'gc> crate::Vm<'gc> {
reader.set_pc(reader.pc() + 1); reader.set_pc(reader.pc() + 1);
Step::Continue(()) Step::Continue(())
} }
#[inline(always)] #[inline(always)]
pub(crate) fn op_has_attr_dynamic( pub(crate) fn op_has_attr_dynamic<'gc, M: MachineExt<'gc>>(
&mut self, 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 {
let (current, dyn_key) = self.force_and_retry::<(StrictValue, StrictValue)>(reader, mc)?; let (current, dyn_key) = m.force_and_retry::<(StrictValue, StrictValue)>(reader, mc)?;
let key_sid = match ctx.get_string_id(dyn_key) { let key_sid = match ctx.get_string_id(dyn_key) {
Ok(id) => id, Ok(id) => id,
Err(got) => return self.finish_type_err(NixType::String, got), Err(got) => return m.finish_type_err(NixType::String, got),
}; };
self.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(),
)); ));
@@ -295,26 +320,26 @@ impl<'gc> crate::Vm<'gc> {
reader.set_pc(reader.pc() + 1); reader.set_pc(reader.pc() + 1);
Step::Continue(()) Step::Continue(())
} }
#[inline(always)] #[inline(always)]
pub(crate) fn op_has_attr_resolve(&mut self) -> 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)
self.push(Value::new_inline(false)); m.push(Value::new(false));
Step::Continue(()) Step::Continue(())
} }
#[inline(always)] #[inline(always)]
pub(crate) fn op_make_list( pub(crate) fn op_make_list<'gc, M: Machine<'gc>>(
&mut self, 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 {
let count = reader.read_u32() as usize; let count = reader.read_u32() as usize;
let mut items: SmallVec<[Value; 4]> = SmallVec::with_capacity(count); let mut items: SmallVec<[Value; 4]> = SmallVec::with_capacity(count);
for _ in 0..count { for _ in 0..count {
items.push(resolve_operand(&reader.read_operand_data(ctx), mc, self)); items.push(resolve_operand(&reader.read_operand_data(), mc, ctx, m));
} }
let list = Gc::new( let list = Gc::new(
mc, mc,
@@ -322,13 +347,12 @@ impl<'gc> crate::Vm<'gc> {
inner: RefLock::new(items), inner: RefLock::new(items),
}, },
); );
self.push(Value::new_gc(list)); m.push(Value::new(list));
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_make_empty_list<'gc, M: Machine<'gc>>(m: &mut M) -> Step {
m.push(m.empty_list());
Step::Continue(()) Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_make_empty_list(&mut self) -> Step {
self.push(self.empty_list);
Step::Continue(())
}
} }
+36 -26
View File
@@ -1,60 +1,70 @@
use fix_abstract_vm::*;
use fix_error::Error; use fix_error::Error;
use fix_runtime::*;
use gc_arena::Mutation; use gc_arena::Mutation;
use crate::{BytecodeReader, Step, VmRuntimeCtx}; use crate::{BytecodeReader, Step, VmRuntimeCtx};
impl<'gc> crate::Vm<'gc> { #[inline(always)]
#[inline(always)] #[expect(
pub(crate) fn op_jump_if_false( clippy::cast_sign_loss,
&mut self, 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>>(
m: &mut M,
reader: &mut BytecodeReader<'_>, reader: &mut BytecodeReader<'_>,
mc: &gc_arena::Mutation<'gc>, mc: &gc_arena::Mutation<'gc>,
) -> Step { ) -> Step {
let offset = reader.read_i32(); let offset = reader.read_i32();
let cond = self.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)]
pub(crate) fn op_jump_if_true( #[expect(
&mut self, 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>>(
m: &mut M,
reader: &mut BytecodeReader<'_>, reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>, mc: &Mutation<'gc>,
) -> Step { ) -> Step {
let offset = reader.read_i32(); let offset = reader.read_i32();
let cond = self.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)]
pub(crate) fn op_jump(&mut self, reader: &mut BytecodeReader<'_>) -> Step { #[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 {
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);
Step::Continue(()) Step::Continue(())
} }
#[inline(always)] #[inline(always)]
pub(crate) fn op_assert( pub(crate) fn op_assert<'gc, M: Machine<'gc>>(
&mut self, 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 {
let raw_id = reader.read_string_id(); let raw_id = reader.read_string_id();
let raw = ctx.resolve_string(raw_id); let raw = ctx.resolve_string(raw_id);
let _span_id = reader.read_u32(); let _span_id = reader.read_u32();
let assertion = self.force_and_retry::<bool>(reader, mc)?; let assertion = m.force_and_retry::<bool>(reader, mc)?;
if !assertion { if !assertion {
// FIXME: use catchable error // FIXME: use catchable error
return self.finish_err(Error::eval_error(format!("assertion '{raw}' failed"))); return m.finish_err(Error::eval_error(format!("assertion '{raw}' failed")));
} }
Step::Continue(()) Step::Continue(())
}
} }
+51 -43
View File
@@ -1,55 +1,63 @@
use fix_runtime::Machine;
use gc_arena::{Gc, Mutation}; use gc_arena::{Gc, Mutation};
use crate::{BytecodeReader, Step, Value}; use crate::{BytecodeReader, Step, Value};
impl<'gc> crate::Vm<'gc> { #[inline(always)]
#[inline(always)] pub(crate) fn op_push_smi<'gc, M: Machine<'gc>>(
pub(crate) fn op_push_smi(&mut self, reader: &mut BytecodeReader<'_>) -> Step { m: &mut M,
reader: &mut BytecodeReader<'_>,
) -> Step {
let val = reader.read_i32(); let val = reader.read_i32();
self.push(Value::new_inline(val)); m.push(Value::new(val));
Step::Continue(()) Step::Continue(())
} }
#[inline(always)] #[inline(always)]
pub(crate) fn op_push_bigint( pub(crate) fn op_push_bigint<'gc, M: Machine<'gc>>(
&mut self, m: &mut M,
reader: &mut BytecodeReader<'_>, reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>, mc: &Mutation<'gc>,
) -> Step { ) -> Step {
let val = reader.read_i64(); let val = reader.read_i64();
self.push(Value::new_gc(Gc::new(mc, val))); m.push(Value::new(Gc::new(mc, val)));
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_push_float<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
) -> Step {
let val = reader.read_f64();
m.push(Value::new(val));
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_push_string<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
) -> Step {
let sid = reader.read_string_id();
m.push(Value::new(sid));
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_push_null<'gc, M: Machine<'gc>>(m: &mut M) -> Step {
m.push(Value::new(crate::Null));
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_push_true<'gc, M: Machine<'gc>>(m: &mut M) -> Step {
m.push(Value::new(true));
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_push_false<'gc, M: Machine<'gc>>(m: &mut M) -> Step {
m.push(Value::new(false));
Step::Continue(()) Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_push_float(&mut self, reader: &mut BytecodeReader<'_>) -> Step {
let val = reader.read_f64();
self.push(Value::new_float(val));
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_push_string(&mut self, reader: &mut BytecodeReader<'_>) -> Step {
let sid = reader.read_string_id();
self.push(Value::new_inline(sid));
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_push_null(&mut self) -> Step {
self.push(Value::new_inline(crate::Null));
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_push_true(&mut self) -> Step {
self.push(Value::new_inline(true));
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_push_false(&mut self) -> Step {
self.push(Value::new_inline(false));
Step::Continue(())
}
} }
+68 -68
View File
@@ -1,105 +1,106 @@
use std::path::PathBuf; use std::path::PathBuf;
use fix_abstract_vm::{ use fix_bytecode::Continuation;
AttrSet, NixString, Path, StrictValue, StringContext, canon_path_str
};
use fix_builtins::BuiltinId;
use fix_common::StringId;
use fix_error::Error; use fix_error::Error;
use num_enum::TryFromPrimitive; use fix_lang::{BuiltinId, StringId};
use fix_runtime::{
AttrSet, Machine, MachineExt, NixString, Path, StrictValue, StringContext, canon_path_str,
};
use crate::{BytecodeReader, PrimOp, Step, Value, VmRuntimeCtx, VmRuntimeCtxExt}; use crate::{BytecodeReader, PrimOp, Step, Value, VmRuntimeCtx, VmRuntimeCtxExt};
impl<'gc> crate::Vm<'gc> { #[inline(always)]
#[inline(always)] pub(crate) fn op_load_builtins<'gc, M: Machine<'gc>>(m: &mut M) -> Step {
pub(crate) fn op_load_builtins(&mut self) -> Step { m.push(m.builtins().into());
self.push(self.builtins);
Step::Continue(()) Step::Continue(())
} }
#[inline(always)] #[inline(always)]
pub(crate) fn op_load_builtin(&mut self, reader: &mut BytecodeReader<'_>) -> Step { #[expect(
let Ok(id) = BuiltinId::try_from_primitive(reader.read_u8()) 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>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
) -> Step {
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));
self.push(Value::new_inline(PrimOp { m.push(Value::new(PrimOp {
id, id,
arity: fix_builtins::BUILTINS[id as usize].1, arity: id.info().arity,
dispatch_ip: id.entry_phase().ip(), dispatch_ip: Continuation::entry_for_builtin(id).ip(),
})); }));
Step::Continue(()) Step::Continue(())
} }
#[inline(always)] #[inline(always)]
pub(crate) fn op_load_repl_binding(&mut self, reader: &mut BytecodeReader<'_>) -> Step { pub(crate) fn op_load_repl_binding<'gc, M: Machine<'gc>>(
_m: &mut M,
reader: &mut BytecodeReader<'_>,
) -> Step {
let _name = reader.read_string_id(); let _name = reader.read_string_id();
todo!("LoadReplBinding"); todo!("LoadReplBinding");
} }
#[inline(always)] #[inline(always)]
pub(crate) fn op_load_scoped_binding( pub(crate) fn op_load_scoped_binding<'gc, M: Machine<'gc>>(
&mut self, m: &mut M,
ctx: &impl VmRuntimeCtx, ctx: &impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>, reader: &mut BytecodeReader<'_>,
_mc: &gc_arena::Mutation<'gc>, _mc: &gc_arena::Mutation<'gc>,
) -> Step { ) -> Step {
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 = match self.scope_slots.get(slot_id as usize).copied() { let scope = m.scope_slot(slot_id);
Some(s) => s, let Some(attrs) = scope.downcast::<AttrSet>() else {
None => { return m.finish_err(Error::eval_error("internal: scope slot is not an attrset"));
return self.finish_err(Error::eval_error(format!(
"internal: invalid scope slot {slot_id}"
)));
}
};
let Some(attrs) = scope.as_gc::<AttrSet>() else {
return self.finish_err(Error::eval_error("internal: scope slot is not an attrset"));
}; };
match attrs.lookup(name) { match attrs.lookup(name) {
Some(val) => { Some(val) => {
self.push(val); m.push(val);
Step::Continue(()) Step::Continue(())
} }
None => self.finish_err(Error::eval_error(format!( None => m.finish_err(Error::eval_error(format!(
"scoped binding '{}' not found", "scoped binding '{}' not found",
ctx.resolve_string(name) ctx.resolve_string(name)
))), ))),
} }
} }
#[inline(always)] #[inline(always)]
pub(crate) fn op_coerce_to_string( pub(crate) fn op_coerce_to_string<'gc, M: Machine<'gc>>(
&mut self, m: &mut M,
reader: &mut BytecodeReader<'_>, reader: &mut BytecodeReader<'_>,
mc: &gc_arena::Mutation<'gc>, mc: &gc_arena::Mutation<'gc>,
) -> Step { ) -> Step {
let val = self.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>() {
self.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
self.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());
} }
Step::Continue(()) Step::Continue(())
} }
#[inline(always)] #[inline(always)]
pub(crate) fn op_concat_strings( pub(crate) fn op_concat_strings<'gc, M: Machine<'gc>>(
&mut self, 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 {
let count = reader.read_u16() as usize; let count = reader.read_u16() as usize;
let _force_string = reader.read_u8() != 0; let _force_string = reader.read_u8() != 0;
let mut total_len = 0; let mut total_len = 0;
let mut has_any_context = false; let mut has_any_context = false;
for i in 0..count { for i in 0..count {
let val = self.peek_forced(count - 1 - i); let val = m.peek_forced(count - 1 - i);
let s = ctx.get_string(val).expect("coerced"); let s = ctx.get_string(val).expect("coerced");
total_len += s.len(); total_len += s.len();
if !ctx.get_string_context(val).is_empty() { if !ctx.get_string_context(val).is_empty() {
@@ -110,7 +111,7 @@ impl<'gc> crate::Vm<'gc> {
let mut result = String::with_capacity(total_len); let mut result = String::with_capacity(total_len);
let mut merged = StringContext::new(); let mut merged = StringContext::new();
for i in 0..count { for i in 0..count {
let val = self.peek_forced(count - 1 - i); let val = m.peek_forced(count - 1 - i);
let s = ctx.get_string(val).expect("coerced"); let s = ctx.get_string(val).expect("coerced");
result.push_str(s); result.push_str(s);
if has_any_context { if has_any_context {
@@ -121,36 +122,36 @@ impl<'gc> crate::Vm<'gc> {
} }
} }
self.stack.truncate(self.stack.len() - count); m.drop_n(count);
if merged.is_empty() { if merged.is_empty() {
let sid = ctx.intern_string(result); let sid = ctx.intern_string(result);
self.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));
self.push(Value::new_gc(ns)); m.push(Value::new(ns));
} }
Step::Continue(()) Step::Continue(())
} }
#[inline(always)] #[inline(always)]
pub(crate) fn op_resolve_path( pub(crate) fn op_resolve_path<'gc, M: MachineExt<'gc>>(
&mut self, 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 {
let path_val = self.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>() {
self.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) {
Some(s) => s.to_owned(), Some(s) => s.to_owned(),
None => { None => {
return self.finish_err(Error::eval_error(format!( return m.finish_err(Error::eval_error(format!(
"expected a string for path, got {}", "expected a string for path, got {}",
path_val.ty() path_val.ty()
))); )));
@@ -158,12 +159,11 @@ impl<'gc> crate::Vm<'gc> {
}; };
let resolved = match resolve_path_str(ctx.resolve_string(dir_id), &path) { let resolved = match resolve_path_str(ctx.resolve_string(dir_id), &path) {
Ok(s) => s, Ok(s) => s,
Err(e) => return self.finish_err(e), Err(e) => return m.finish_err(e),
}; };
let sid = ctx.intern_string(resolved); let sid = ctx.intern_string(resolved);
self.push(Value::new_inline(Path(sid))); m.push(Value::new(Path(sid)));
Step::Continue(()) Step::Continue(())
}
} }
fn resolve_path_str(current_dir: &str, path: &str) -> Result<String, Box<Error>> { fn resolve_path_str(current_dir: &str, path: &str) -> Result<String, Box<Error>> {
+10
View File
@@ -7,3 +7,13 @@ pub(crate) mod literals;
pub(crate) mod misc; pub(crate) mod misc;
pub(crate) mod variables; pub(crate) mod variables;
pub(crate) mod with_scope; pub(crate) mod with_scope;
pub(crate) use arithmetic::*;
pub(crate) use calls::*;
pub(crate) use closures::*;
pub(crate) use collections::*;
pub(crate) use control::*;
pub(crate) use literals::*;
pub(crate) use misc::*;
pub(crate) use variables::*;
pub(crate) use with_scope::*;
+41 -23
View File
@@ -1,50 +1,68 @@
use fix_runtime::Machine;
use crate::{BytecodeReader, Mutation, Step, Value}; use crate::{BytecodeReader, Mutation, Step, Value};
impl<'gc> crate::Vm<'gc> { #[inline(always)]
#[inline(always)] #[expect(
pub(crate) fn op_load_local(&mut self, reader: &mut BytecodeReader<'_>) -> Step { 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>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
) -> Step {
let idx = reader.read_u32() as usize; let idx = reader.read_u32() as usize;
self.push(self.env.borrow().locals[idx]); m.push(m.env().borrow().locals[idx]);
Step::Continue(()) Step::Continue(())
} }
#[inline(always)] #[inline(always)]
pub(crate) fn op_load_outer(&mut self, reader: &mut BytecodeReader<'_>) -> Step { #[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>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
) -> Step {
let layer = reader.read_u8(); let layer = reader.read_u8();
let idx = reader.read_u32() as usize; let idx = reader.read_u32() as usize;
let mut cur = self.env; let mut cur = m.env();
for _ in 0..layer { for _ in 0..layer {
let prev = cur.borrow().prev.expect("LoadOuter: env chain too short"); let prev = cur.borrow().prev.expect("LoadOuter: env chain too short");
cur = prev; cur = prev;
} }
let val = cur.borrow().locals[idx]; let val = cur.borrow().locals[idx];
self.push(val); m.push(val);
Step::Continue(()) Step::Continue(())
} }
#[inline(always)] #[inline(always)]
pub(crate) fn op_store_local( #[expect(
&mut self, 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>>(
m: &mut M,
reader: &mut BytecodeReader<'_>, reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>, mc: &Mutation<'gc>,
) -> Step { ) -> Step {
let idx = reader.read_u32() as usize; let idx = reader.read_u32() as usize;
let val = self.pop(); let val = m.pop();
self.env.borrow_mut(mc).locals[idx] = val; m.env().borrow_mut(mc).locals[idx] = val;
Step::Continue(()) Step::Continue(())
} }
#[inline(always)] #[inline(always)]
pub(crate) fn op_alloc_locals( pub(crate) fn op_alloc_locals<'gc, M: Machine<'gc>>(
&mut self, m: &mut M,
reader: &mut BytecodeReader<'_>, reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>, mc: &Mutation<'gc>,
) -> Step { ) -> Step {
let count = reader.read_u32() as usize; let count = reader.read_u32() as usize;
self.env m.env()
.borrow_mut(mc) .borrow_mut(mc)
.locals .locals
.extend(std::iter::repeat_n(Value::default(), count)); .extend(std::iter::repeat_n(Value::default(), count));
Step::Continue(()) Step::Continue(())
}
} }
+30 -24
View File
@@ -1,26 +1,32 @@
use fix_abstract_vm::{resolve_operand, *};
use fix_common::Symbol;
use fix_error::Error; use fix_error::Error;
use fix_lang::Symbol;
use fix_runtime::{resolve_operand, *};
use smallvec::SmallVec; use smallvec::SmallVec;
use crate::{Break, BytecodeReader, CallFrame, Step, VmRuntimeCtx}; use crate::{Break, BytecodeReader, CallFrame, Step, VmRuntimeCtx};
impl<'gc> crate::Vm<'gc> { #[inline(always)]
#[inline(always)] #[expect(
pub(crate) fn op_lookup_with( clippy::indexing_slicing,
&mut self, 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>>(
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 = self.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();
let mut namespaces = SmallVec::<[_; 2]>::new(); let mut namespaces = SmallVec::<[_; 2]>::new();
for _ in 0..n { for _ in 0..n {
namespaces.push(resolve_operand(&reader.read_operand_data(ctx), mc, self)); namespaces.push(resolve_operand(&reader.read_operand_data(), mc, ctx, m));
} }
let resume_pc = reader.inst_start_pc(); let resume_pc = reader.inst_start_pc();
@@ -31,48 +37,48 @@ impl<'gc> crate::Vm<'gc> {
match *state { match *state {
ThunkState::Pending { ip, env } => { ThunkState::Pending { ip, env } => {
*state = ThunkState::Blackhole; *state = ThunkState::Blackhole;
self.call_stack.push(CallFrame { m.push_call_frame(CallFrame {
thunk: Some(thunk), thunk: Some(thunk),
pc: resume_pc, pc: resume_pc,
env: self.env, env: m.env(),
depth: None,
}); });
self.env = env; m.set_env(env);
reader.set_pc(ip); reader.set_pc(ip);
return Step::Break(Break::Force); return Step::Break(Break::Force);
} }
ThunkState::Evaluated(v) => v, ThunkState::Evaluated(v) => v,
ThunkState::Apply { func, arg } => { ThunkState::Apply { func, arg } => {
self.call_stack.push(CallFrame { m.push_call_frame(CallFrame {
thunk: Some(thunk), thunk: Some(thunk),
pc: resume_pc, pc: resume_pc,
env: self.env, env: m.env(),
depth: None,
}); });
self.push(func); m.push(func);
return self.call(reader, mc, arg, resume_pc); return m.call(reader, mc, arg, resume_pc);
} }
ThunkState::Blackhole => { ThunkState::Blackhole => {
return self return m.finish_err(Error::eval_error("infinite recursion encountered"));
.finish_err(Error::eval_error("infinite recursion encountered"));
} }
} }
} }
}; };
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))
{ {
self.replace(0, val); m.replace(0, val);
} else if counter + 1 == n as i32 { } else if counter + 1 == n as i32 {
return self.finish_err(Error::eval_error(format!( return m.finish_err(Error::eval_error(format!(
"undefined variable '{}'", "undefined variable '{}'",
Symbol::from(ctx.resolve_string(name)) Symbol::from(ctx.resolve_string(name))
))); )));
} else { } else {
self.replace(0, Value::new_inline(counter + 1)); m.replace(0, Value::new(counter + 1));
reader.set_pc(resume_pc); reader.set_pc(resume_pc);
} }
Step::Continue(()) Step::Continue(())
}
} }
+163 -272
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,20 +9,23 @@
use std::path::PathBuf; use std::path::PathBuf;
use fix_builtins::{BUILTINS, BuiltinId}; use fix_bytecode::{Continuation, InstructionPtr};
use fix_codegen::InstructionPtr;
use fix_common::StringId;
use fix_error::{Error, Result, Source}; use fix_error::{Error, Result, Source};
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;
use num_enum::TryFromPrimitive;
use smallvec::SmallVec; use smallvec::SmallVec;
#[cfg(feature = "tailcall")] #[cfg(feature = "tailcall")]
mod dispatch_tailcall; mod dispatch_tailcall;
pub use fix_abstract_vm::*; pub use fix_runtime::*;
#[doc(hidden)]
#[path = "macro_support.rs"]
pub mod __macro_support;
mod instructions; mod instructions;
mod primops;
extern crate self as fix_vm;
type VmResult<T> = std::result::Result<T, VmError>; type VmResult<T> = std::result::Result<T, VmError>;
@@ -30,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>,
@@ -39,14 +47,14 @@ 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>,
force_mode: ForceMode, force_mode: ForceMode,
#[collect(require_static)] #[collect(require_static)]
result: Option<Result<fix_common::Value>>, result: Option<Result<fix_lang::Value>>,
#[collect(require_static)] #[collect(require_static)]
pending_load: Option<PendingLoad>, pending_load: Option<PendingLoad>,
@@ -54,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_primitive(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 = id.entry_phase().ip();
entries.push(( entries.push((
name, name,
Value::new_inline(PrimOp { Value::new(PrimOp {
id, id,
arity, arity,
dispatch_ip, dispatch_ip,
@@ -75,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 {
@@ -100,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> {
@@ -126,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,
@@ -137,45 +138,20 @@ impl<'gc> Vm<'gc> {
functor_sym: ctx.intern_string("__functor"), functor_sym: ctx.intern_string("__functor"),
} }
} }
}
#[inline(always)] impl<'gc> Machine<'gc> for Vm<'gc> {
fn finish_ok(&mut self, val: fix_common::Value) -> Step {
self.result = Some(Ok(val));
Step::Break(Break::Done)
}
#[inline(always)]
fn finish_err(&mut self, err: Box<Error>) -> Step {
self.result = Some(Err(err));
Step::Break(Break::Done)
}
#[inline(always)]
fn finish_type_err(&mut self, expected: NixType, got: NixType) -> Step {
self.result = Some(Err(Error::eval_error(format!(
"expected {expected}, got {got}"
))));
Step::Break(Break::Done)
}
#[inline(always)]
fn finish_vm_err(&mut self, err: VmError) -> Step {
self.finish_err(err.into_error())
}
#[inline(always)] #[inline(always)]
fn push(&mut self, val: Value<'gc>) { fn push(&mut self, val: Value<'gc>) {
self.stack.push(val); self.stack.push(val);
} }
#[inline(always)] #[inline(always)]
#[must_use]
fn pop(&mut self) -> Value<'gc> { fn pop(&mut self) -> Value<'gc> {
self.stack.pop().expect("stack underflow") self.stack.pop().expect("stack underflow")
} }
#[inline(always)] #[inline(always)]
#[must_use]
fn peek(&self, depth: usize) -> Value<'gc> { fn peek(&self, depth: usize) -> Value<'gc> {
*self *self
.stack .stack
@@ -184,7 +160,6 @@ impl<'gc> Vm<'gc> {
} }
#[inline(always)] #[inline(always)]
#[must_use]
fn peek_forced(&self, depth: usize) -> StrictValue<'gc> { fn peek_forced(&self, depth: usize) -> StrictValue<'gc> {
self.stack self.stack
.get(self.stack.len() - depth - 1) .get(self.stack.len() - depth - 1)
@@ -193,6 +168,15 @@ impl<'gc> Vm<'gc> {
.expect("forced") .expect("forced")
} }
#[inline(always)]
fn pop_forced(&mut self) -> StrictValue<'gc> {
self.stack
.pop()
.expect("stack underflow")
.restrict()
.expect("forced")
}
#[inline(always)] #[inline(always)]
fn replace(&mut self, depth: usize, val: Value<'gc>) { fn replace(&mut self, depth: usize, val: Value<'gc>) {
let len = self.stack.len(); let len = self.stack.len();
@@ -203,147 +187,8 @@ impl<'gc> Vm<'gc> {
} }
#[inline(always)] #[inline(always)]
#[cfg_attr(debug_assertions, track_caller)] fn drop_n(&mut self, depth: usize) {
fn pop_forced(&mut self) -> StrictValue<'gc> { self.stack.truncate(self.stack.len() - depth);
self.stack
.pop()
.expect("stack underflow")
.restrict()
.expect("forced")
}
/// Force the top `T::WIDTH` stack slots and return them as `T`.
///
/// If any slot holds a pending thunk, this method pushes a call frame
/// whose resume PC is the **start of the current instruction**
/// (`reader.inst_start_pc()`), enters the thunk, and returns
/// `Break::Force`. When the thunk eventually returns, the VM will
/// **re-execute the entire opcode handler from the beginning**.
///
/// # Invariants
///
/// * **Do not call this method more than once in a single handler.**
/// If you need to force multiple values, use a tuple type such as
/// `(StrictValue, StrictValue)` so they are forced and popped in one
/// atomic operation. Calling `force_and_retry` twice (or more)
/// means the handler will be re-run from the top after each retry;
/// any stack modifications between the two calls would be duplicated
/// and corrupt the stack layout.
///
/// * The caller must ensure that the stack layout at the point of
/// invocation is **identical** every time the handler is re-entered.
/// In practice this means no pushes, pops, or local mutations may
/// happen before the call, and the call must be the first thing
/// that consumes the instruction's operand values.
///
/// * The return value must be propagated with `?` so that
/// `Break::Force` correctly unwinds to the dispatch loop.
#[inline(always)]
fn force_and_retry<T: Forced<'gc>>(
&mut self,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> std::ops::ControlFlow<Break, T> {
self.force_and_retry_pc(reader, mc, reader.inst_start_pc())
}
/// Same as [`force_and_retry`](Self::force_and_retry) but allows
/// specifying a custom resume PC.
#[inline(always)]
fn force_and_retry_pc<T: Forced<'gc>>(
&mut self,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
resume_pc: usize,
) -> std::ops::ControlFlow<Break, T> {
T::force_and_check(self, reader, mc, 0, resume_pc)?;
std::ops::ControlFlow::Continue(T::pop_converted(self))
}
#[inline(always)]
#[allow(unused)]
fn force_slot(
&mut self,
depth: usize,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
self.force_slot_to_pc(depth, reader, mc, reader.inst_start_pc())
}
#[inline(always)]
fn force_slot_to_pc(
&mut self,
depth: usize,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
resume_pc: usize,
) -> Step {
let Some(thunk) = self.peek(depth).as_gc::<Thunk>() else {
return Step::Continue(());
};
let mut state = thunk.borrow_mut(mc);
match *state {
ThunkState::Pending { ip, env } => {
*state = ThunkState::Blackhole;
self.call_stack.push(CallFrame {
thunk: Some(thunk),
pc: resume_pc,
env: self.env,
});
self.env = env;
reader.set_pc(ip);
Step::Break(Break::Force)
}
ThunkState::Evaluated(v) => {
self.replace(depth, v.relax());
Step::Continue(())
}
ThunkState::Apply { func, arg } => {
self.call_stack.push(CallFrame {
thunk: Some(thunk),
pc: resume_pc,
env: self.env,
});
self.push(func);
self.call(reader, mc, arg, resume_pc)
}
ThunkState::Blackhole => {
self.finish_err(Error::eval_error("infinite recursion encountered"))
}
}
}
}
impl<'gc> Machine<'gc> for Vm<'gc> {
#[inline(always)]
fn push(&mut self, val: Value<'gc>) {
self.push(val);
}
#[inline(always)]
fn pop(&mut self) -> Value<'gc> {
self.pop()
}
#[inline(always)]
fn peek(&self, depth: usize) -> Value<'gc> {
Vm::peek(self, depth)
}
#[inline(always)]
fn peek_forced(&self, depth: usize) -> StrictValue<'gc> {
Vm::peek_forced(self, depth)
}
#[inline(always)]
fn pop_forced(&mut self) -> StrictValue<'gc> {
self.pop_forced()
}
#[inline(always)]
fn replace(&mut self, depth: usize, val: Value<'gc>) {
self.replace(depth, val);
} }
#[inline(always)] #[inline(always)]
@@ -359,7 +204,41 @@ impl<'gc> Machine<'gc> for Vm<'gc> {
mc: &Mutation<'gc>, mc: &Mutation<'gc>,
resume_pc: usize, resume_pc: usize,
) -> Step { ) -> Step {
self.force_slot_to_pc(depth, reader, mc, resume_pc) let Some(thunk) = self.peek(depth).downcast::<Thunk>() else {
return Step::Continue(());
};
let mut state = thunk.borrow_mut(mc);
match *state {
ThunkState::Pending { ip, env } => {
*state = ThunkState::Blackhole;
self.call_stack.push(CallFrame {
thunk: Some(thunk),
pc: resume_pc,
env: self.env,
depth: Some(depth),
});
self.env = env;
reader.set_pc(ip);
Step::Break(Break::Force)
}
ThunkState::Evaluated(v) => {
self.replace(depth, v.relax());
Step::Continue(())
}
ThunkState::Apply { func, arg } => {
self.call_stack.push(CallFrame {
thunk: Some(thunk),
pc: resume_pc,
env: self.env,
depth: Some(depth),
});
self.push(func);
self.call(reader, mc, arg, resume_pc)
}
ThunkState::Blackhole => {
self.finish_err(Error::eval_error("infinite recursion encountered"))
}
}
} }
#[inline(always)] #[inline(always)]
@@ -370,7 +249,7 @@ impl<'gc> Machine<'gc> for Vm<'gc> {
arg: Value<'gc>, arg: Value<'gc>,
resume_pc: usize, resume_pc: usize,
) -> Step { ) -> Step {
self.call(reader, mc, arg, resume_pc) instructions::call(self, reader, mc, arg, resume_pc)
} }
#[inline(always)] #[inline(always)]
@@ -409,22 +288,27 @@ impl<'gc> Machine<'gc> for Vm<'gc> {
} }
#[inline(always)] #[inline(always)]
fn finish_ok(&mut self, val: fix_common::Value) -> Step { fn finish_ok(&mut self, val: fix_lang::Value) -> Step {
self.finish_ok(val) self.result = Some(Ok(val));
Step::Break(Break::Done)
} }
#[inline(always)] #[inline(always)]
fn finish_err(&mut self, err: Box<Error>) -> Step { fn finish_err(&mut self, err: Box<Error>) -> Step {
self.finish_err(err) self.result = Some(Err(err));
Step::Break(Break::Done)
} }
#[inline(always)] #[inline(always)]
fn finish_type_err(&mut self, expected: NixType, got: NixType) -> Step { fn finish_type_err(&mut self, expected: NixType, got: NixType) -> Step {
self.finish_type_err(expected, got) self.result = Some(Err(Error::eval_error(format!(
"expected {expected}, got {got}"
))));
Step::Break(Break::Done)
} }
#[inline(always)] #[inline(always)]
fn builtins(&self) -> Value<'gc> { fn builtins(&self) -> Gc<'gc, AttrSet<'gc>> {
self.builtins self.builtins
} }
@@ -481,7 +365,7 @@ impl<'gc> Machine<'gc> for Vm<'gc> {
enum Action { enum Action {
Continue { pc: usize }, Continue { pc: usize },
Done(Result<fix_common::Value>), Done(Result<fix_lang::Value>),
LoadFile(PendingLoad), LoadFile(PendingLoad),
} }
@@ -504,7 +388,7 @@ impl Vm<'_> {
ctx: &mut C, ctx: &mut C,
ip: InstructionPtr, ip: InstructionPtr,
force_mode: ForceMode, force_mode: ForceMode,
) -> Result<fix_common::Value> { ) -> Result<fix_lang::Value> {
let (code, runtime) = ctx.split(); let (code, runtime) = ctx.split();
let mut arena: Arena<Rootable![Vm<'_>]> = Arena::new(|mc| Vm::new(force_mode, mc, runtime)); let mut arena: Arena<Rootable![Vm<'_>]> = Arena::new(|mc| Vm::new(force_mode, mc, runtime));
arena.metrics().set_pacing(Pacing { arena.metrics().set_pacing(Pacing {
@@ -582,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],
@@ -589,12 +477,15 @@ impl<'gc> Vm<'gc> {
pc: usize, pc: usize,
mc: &Mutation<'gc>, mc: &Mutation<'gc>,
) -> Action { ) -> Action {
use fix_codegen::Op::*; use fix_bytecode::Op::*;
use instructions::*;
let mut reader = BytecodeReader::new(bytecode, pc); let mut reader = BytecodeReader::new(bytecode, pc);
let mut fuel = Self::DEFAULT_FUEL_AMOUNT; let mut fuel = Self::DEFAULT_FUEL_AMOUNT;
loop { loop {
use crate::instructions::op_dispatch_cont;
if fuel == 0 { if fuel == 0 {
return Action::Continue { pc: reader.pc() }; return Action::Continue { pc: reader.pc() };
} }
@@ -603,75 +494,75 @@ impl<'gc> Vm<'gc> {
let op = reader.read_op(); let op = reader.read_op();
let result = match op { let result = match op {
PushSmi => self.op_push_smi(&mut reader), PushSmi => op_push_smi(self, &mut reader),
PushBigInt => self.op_push_bigint(&mut reader, mc), PushBigInt => op_push_bigint(self, &mut reader, mc),
PushFloat => self.op_push_float(&mut reader), PushFloat => op_push_float(self, &mut reader),
PushString => self.op_push_string(&mut reader), PushString => op_push_string(self, &mut reader),
PushNull => self.op_push_null(), PushNull => op_push_null(self),
PushTrue => self.op_push_true(), PushTrue => op_push_true(self),
PushFalse => self.op_push_false(), PushFalse => op_push_false(self),
LoadLocal => self.op_load_local(&mut reader), LoadLocal => op_load_local(self, &mut reader),
LoadOuter => self.op_load_outer(&mut reader), LoadOuter => op_load_outer(self, &mut reader),
StoreLocal => self.op_store_local(&mut reader, mc), StoreLocal => op_store_local(self, &mut reader, mc),
AllocLocals => self.op_alloc_locals(&mut reader, mc), AllocLocals => op_alloc_locals(self, &mut reader, mc),
MakeThunk => self.op_make_thunk(&mut reader, mc), MakeThunk => op_make_thunk(self, &mut reader, mc),
MakeClosure => self.op_make_closure(&mut reader, mc), MakeClosure => op_make_closure(self, &mut reader, mc),
MakePatternClosure => self.op_make_pattern_closure(&mut reader, mc), MakePatternClosure => op_make_pattern_closure(self, &mut reader, mc),
Call => self.op_call(ctx, &mut reader, mc), Call => op_call(self, ctx, &mut reader, mc),
DispatchPrimOp => self.op_dispatch_primop(ctx, &mut reader, mc), DispatchCont => op_dispatch_cont(self, ctx, &mut reader, mc),
Return => self.op_return(ctx, &mut reader, mc), Return => op_return(self, ctx, &mut reader, mc),
MakeAttrs => self.op_make_attrs(ctx, &mut reader, mc), MakeAttrs => op_make_attrs(self, ctx, &mut reader, mc),
MakeEmptyAttrs => self.op_make_empty_attrs(), MakeEmptyAttrs => op_make_empty_attrs(self),
SelectStatic => self.op_select_static(ctx, &mut reader, mc), SelectStatic => op_select_static(self, ctx, &mut reader, mc),
SelectDynamic => self.op_select_dynamic(ctx, &mut reader, mc), SelectDynamic => op_select_dynamic(self, ctx, &mut reader, mc),
HasAttrPathStatic => self.op_has_attr_path_static(ctx, &mut reader, mc), HasAttrPathStatic => op_has_attr_path_static(self, ctx, &mut reader, mc),
HasAttrPathDynamic => self.op_has_attr_path_dynamic(ctx, &mut reader, mc), HasAttrPathDynamic => op_has_attr_path_dynamic(self, ctx, &mut reader, mc),
HasAttrStatic => self.op_has_attr_static(&mut reader, mc), HasAttrStatic => op_has_attr_static(self, &mut reader, mc),
HasAttrDynamic => self.op_has_attr_dynamic(ctx, &mut reader, mc), HasAttrDynamic => op_has_attr_dynamic(self, ctx, &mut reader, mc),
HasAttrResolve => self.op_has_attr_resolve(), HasAttrResolve => op_has_attr_resolve(self),
JumpIfSelectFailed => self.op_jump_if_select_failed(&mut reader), JumpIfSelectFailed => op_jump_if_select_failed(self, &mut reader),
JumpIfSelectSucceeded => self.op_jump_if_select_succeeded(&mut reader), JumpIfSelectSucceeded => op_jump_if_select_succeeded(self, &mut reader),
MakeList => self.op_make_list(ctx, &mut reader, mc), MakeList => op_make_list(self, ctx, &mut reader, mc),
MakeEmptyList => self.op_make_empty_list(), MakeEmptyList => op_make_empty_list(self),
OpAdd => self.op_add(ctx, &mut reader, mc), OpAdd => op_add(self, ctx, &mut reader, mc),
OpSub => self.op_sub(&mut reader, mc), OpSub => op_sub(self, &mut reader, mc),
OpMul => self.op_mul(&mut reader, mc), OpMul => op_mul(self, &mut reader, mc),
OpDiv => self.op_div(&mut reader, mc), OpDiv => op_div(self, &mut reader, mc),
OpEq => self.op_eq(ctx, &mut reader, mc), OpEq => op_eq(self, ctx, &mut reader, mc),
OpNeq => self.op_neq(ctx, &mut reader, mc), OpNeq => op_neq(self, ctx, &mut reader, mc),
OpLt => self.op_lt(ctx, &mut reader, mc), OpLt => op_lt(self, ctx, &mut reader, mc),
OpGt => self.op_gt(ctx, &mut reader, mc), OpGt => op_gt(self, ctx, &mut reader, mc),
OpLeq => self.op_leq(ctx, &mut reader, mc), OpLeq => op_leq(self, ctx, &mut reader, mc),
OpGeq => self.op_geq(ctx, &mut reader, mc), OpGeq => op_geq(self, ctx, &mut reader, mc),
OpConcat => self.op_concat(&mut reader, mc), OpConcat => op_concat(self, &mut reader, mc),
OpUpdate => self.op_update(&mut reader, mc), OpUpdate => op_update(self, &mut reader, mc),
OpNeg => self.op_neg(&mut reader, mc), OpNeg => op_neg(self, &mut reader, mc),
OpNot => self.op_not(&mut reader, mc), OpNot => op_not(self, &mut reader, mc),
JumpIfFalse => self.op_jump_if_false(&mut reader, mc), JumpIfFalse => op_jump_if_false(self, &mut reader, mc),
JumpIfTrue => self.op_jump_if_true(&mut reader, mc), JumpIfTrue => op_jump_if_true(self, &mut reader, mc),
Jump => self.op_jump(&mut reader), Jump => op_jump(self, &mut reader),
ConcatStrings => self.op_concat_strings(ctx, &mut reader, mc), ConcatStrings => op_concat_strings(self, ctx, &mut reader, mc),
CoerceToString => self.op_coerce_to_string(&mut reader, mc), CoerceToString => op_coerce_to_string(self, &mut reader, mc),
ResolvePath => self.op_resolve_path(ctx, &mut reader, mc), ResolvePath => op_resolve_path(self, ctx, &mut reader, mc),
Assert => self.op_assert(ctx, &mut reader, mc), Assert => op_assert(self, ctx, &mut reader, mc),
LookupWith => self.op_lookup_with(ctx, &mut reader, mc), LookupWith => op_lookup_with(self, ctx, &mut reader, mc),
LoadBuiltins => self.op_load_builtins(), LoadBuiltins => op_load_builtins(self),
LoadBuiltin => self.op_load_builtin(&mut reader), LoadBuiltin => op_load_builtin(self, &mut reader),
LoadReplBinding => self.op_load_repl_binding(&mut reader), LoadReplBinding => op_load_repl_binding(self, &mut reader),
LoadScopedBinding => self.op_load_scoped_binding(ctx, &mut reader, mc), LoadScopedBinding => op_load_scoped_binding(self, ctx, &mut reader, mc),
Illegal => unreachable!(), Illegal => unreachable!(),
}; };
+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")
}
@@ -2,16 +2,16 @@
//! `builtins.unsafeDiscardStringContext`, //! `builtins.unsafeDiscardStringContext`,
//! `builtins.unsafeDiscardOutputDependency`. //! `builtins.unsafeDiscardOutputDependency`.
//! //!
//! See `fix-abstract-vm/src/string_context.rs` for the //! See `fix-runtime/src/string_context.rs` for the
//! `StringContextElem` type. //! `StringContextElem` type.
use fix_abstract_vm::{ use fix_bytecode::Continuation;
use fix_error::Error;
use fix_lang::StringId;
use fix_runtime::{
AttrSet, BytecodeReader, List as VmList, Machine, MachineExt, NixString, NixType, Step, AttrSet, BytecodeReader, List as VmList, Machine, MachineExt, NixString, NixType, Step,
StrictValue, StringContext, StringContextElem, Value, VmRuntimeCtx, VmRuntimeCtxExt, StrictValue, StringContext, StringContextElem, Value, VmRuntimeCtx, VmRuntimeCtxExt,
}; };
use fix_builtins::PrimOpPhase;
use fix_common::StringId;
use fix_error::Error;
use gc_arena::{Gc, Mutation}; use gc_arena::{Gc, Mutation};
use smallvec::SmallVec; use smallvec::SmallVec;
@@ -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(PrimOpPhase::AppendContextLoop.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);
@@ -221,12 +230,17 @@ pub fn append_context_loop<'gc, M: Machine<'gc>>(
0, 0,
reader, reader,
mc, mc,
PrimOpPhase::AppendContextEntryForced.ip() as usize, Continuation::PAppendContextEntryForced.ip() as usize,
)?; )?;
reader.set_pc(PrimOpPhase::AppendContextEntryForced.ip() as usize); reader.set_pc(Continuation::PAppendContextEntryForced.ip() as usize);
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);
@@ -296,17 +316,19 @@ pub fn append_context_entry_forced<'gc, M: Machine<'gc>>(
0, 0,
reader, reader,
mc, mc,
PrimOpPhase::AppendContextOutputsForced.ip() as usize, Continuation::PAppendContextOutputsForced.ip() as usize,
)?; )?;
reader.set_pc(PrimOpPhase::AppendContextOutputsForced.ip() as usize); reader.set_pc(Continuation::PAppendContextOutputsForced.ip() as usize);
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>()
reader.set_pc(PrimOpPhase::AppendContextLoop.ip() as usize); .expect("stack slot must be an integer");
m.replace(1, Value::new(idx_back + 1));
reader.set_pc(Continuation::PAppendContextLoop.ip() as usize);
Step::Continue(()) Step::Continue(())
} }
@@ -318,44 +340,56 @@ 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>()
reader.set_pc(PrimOpPhase::AppendContextLoop.ip() as usize); .expect("stack slot must be an integer");
m.replace(1, Value::new(idx_back + 1));
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(PrimOpPhase::AppendContextOutputElementLoop.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");
reader.set_pc(PrimOpPhase::AppendContextLoop.ip() as usize); m.replace(1, Value::new(idx_back + 1));
reader.set_pc(Continuation::PAppendContextLoop.ip() as usize);
return Step::Continue(()); return Step::Continue(());
} }
@@ -365,12 +399,17 @@ pub fn append_context_output_element_loop<'gc, M: Machine<'gc>>(
0, 0,
reader, reader,
mc, mc,
PrimOpPhase::AppendContextOutputElementForced.ip() as usize, Continuation::PAppendContextOutputElementForced.ip() as usize,
)?; )?;
reader.set_pc(PrimOpPhase::AppendContextOutputElementForced.ip() as usize); reader.set_pc(Continuation::PAppendContextOutputElementForced.ip() as usize);
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>()
reader.set_pc(PrimOpPhase::AppendContextOutputElementLoop.ip() as usize); .expect("stack slot must be an integer");
m.replace(0, Value::new(oidx + 1));
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)
} }
@@ -1,22 +1,89 @@
use fix_abstract_vm::{ use fix_bytecode::Continuation;
AttrSet, BytecodeReader, Closure, Env, List, Machine, MachineExt, Step, StrictValue, Value, use fix_error::{Error, Result};
VmRuntimeCtx, VmRuntimeCtxExt, use fix_macros::handler;
use fix_runtime::{
AttrSet, BytecodeReader, Closure, Env, List, Machine, MachineExt, Slot, Step, StrictValue,
Value, VmRuntimeCtx, VmRuntimeCtxExt,
}; };
use fix_builtins::PrimOpPhase;
use fix_error::Error;
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(PrimOpPhase::DeepSeqPush.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, PrimOpPhase::DeepSeqLoop.ip() as usize)?;
reader.set_pc(PrimOpPhase::DeepSeqLoop.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(PrimOpPhase::DeepSeqPush.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,49 +125,56 @@ 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(PrimOpPhase::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,
reader, reader,
mc, mc,
PrimOpPhase::ForceResultShallowLoop.ip() as usize, Continuation::ForceResultShallowLoop.ip() as usize,
)?; )?;
reader.set_pc(PrimOpPhase::ForceResultShallowLoop.ip() as usize); reader.set_pc(Continuation::ForceResultShallowLoop.ip() as usize);
} }
Step::Continue(()) Step::Continue(())
} }
@@ -237,8 +184,8 @@ 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(PrimOpPhase::ForceResultShallowPush.ip() as usize); reader.set_pc(Continuation::ForceResultShallowPush.ip() as usize);
Step::Continue(()) Step::Continue(())
} }
@@ -288,7 +235,7 @@ pub fn call_functor_1<'gc, M: Machine<'gc>>(
reader, reader,
mc, mc,
self_val, self_val,
PrimOpPhase::CallFunctor2.ip() as usize, Continuation::CallFunctor2.ip() as usize,
) )
} }
@@ -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);
@@ -1,9 +1,9 @@
use fix_abstract_vm::{ use fix_error::Error;
use fix_lang::StringId;
use fix_runtime::{
BytecodeReader, Machine, MachineExt, NixString, NixType, Path, Step, StrictValue, Value, BytecodeReader, Machine, MachineExt, NixString, NixType, Path, Step, StrictValue, Value,
VmRuntimeCtx, VmRuntimeCtx,
}; };
use fix_common::StringId;
use fix_error::Error;
use gc_arena::Mutation; use gc_arena::Mutation;
pub fn to_string<'gc, M: Machine<'gc>>( pub fn to_string<'gc, M: Machine<'gc>>(
@@ -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)
} }
@@ -1,8 +1,8 @@
use fix_abstract_vm::{ use fix_bytecode::Continuation;
use fix_runtime::{
AttrSet, BytecodeReader, CallFrame, List, Machine, MachineExt, NixNum, Null, Path, Step, AttrSet, BytecodeReader, CallFrame, List, Machine, MachineExt, NixNum, Null, Path, Step,
StrictValue, Value, VmRuntimeCtx, VmRuntimeCtxExt, StrictValue, Value, VmRuntimeCtx, VmRuntimeCtxExt,
}; };
use fix_builtins::PrimOpPhase;
use gc_arena::{Gc, Mutation}; use gc_arena::{Gc, Mutation};
use smallvec::SmallVec; use smallvec::SmallVec;
@@ -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) => {
@@ -30,10 +30,8 @@ pub fn start_eq<'gc, M: Machine<'gc>>(
enter_eq_machine(m, reader, mc, negate, lhs_init, rhs_init) enter_eq_machine(m, reader, mc, negate, lhs_init, rhs_init)
} }
ShallowEq::RecurseAttrs(a, b) => { ShallowEq::RecurseAttrs(a, b) => {
let lhs_init: SmallVec<[Value<'gc>; 4]> = let lhs_init: SmallVec<[Value<'gc>; 4]> = a.entries.iter().map(|&(_, v)| v).collect();
a.entries.iter().map(|&(_, v)| v).collect(); let rhs_init: SmallVec<[Value<'gc>; 4]> = b.entries.iter().map(|&(_, v)| v).collect();
let rhs_init: SmallVec<[Value<'gc>; 4]> =
b.entries.iter().map(|&(_, v)| v).collect();
enter_eq_machine(m, reader, mc, negate, lhs_init, rhs_init) enter_eq_machine(m, reader, mc, negate, lhs_init, rhs_init)
} }
} }
@@ -46,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() {
@@ -73,7 +71,7 @@ pub fn eq_step<'gc, M: Machine<'gc>>(
.expect("non-empty rhs queue"); .expect("non-empty rhs queue");
m.push(lhs); m.push(lhs);
m.push(rhs); m.push(rhs);
reader.set_pc(PrimOpPhase::EqForce.ip() as usize); reader.set_pc(Continuation::EqForce.ip() as usize);
Step::Continue(()) Step::Continue(())
} }
@@ -85,22 +83,21 @@ pub fn eq_force<'gc, M: Machine<'gc>>(
) -> Step { ) -> Step {
let (lhs, rhs) = m.force_and_retry::<(StrictValue, StrictValue)>(reader, mc)?; let (lhs, rhs) = m.force_and_retry::<(StrictValue, StrictValue)>(reader, mc)?;
apply_pair(m, ctx, mc, lhs, rhs); apply_pair(m, ctx, mc, lhs, rhs);
reader.set_pc(PrimOpPhase::EqStep.ip() as usize); reader.set_pc(Continuation::EqStep.ip() as usize);
Step::Continue(()) Step::Continue(())
} }
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>>(
@@ -113,10 +110,15 @@ 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(m, mc, la.inner.borrow().iter().copied(), lb.inner.borrow().iter().copied()); extend_queues(
m,
mc,
la.inner.borrow().iter().copied(),
lb.inner.borrow().iter().copied(),
);
} }
ShallowEq::RecurseAttrs(a, b) => { ShallowEq::RecurseAttrs(a, b) => {
extend_queues( extend_queues(
@@ -137,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();
@@ -164,13 +166,14 @@ 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(PrimOpPhase::EqStep.ip() as usize); reader.set_pc(Continuation::EqStep.ip() as usize);
Step::Continue(()) Step::Continue(())
} }
@@ -186,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,
@@ -195,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() {
@@ -230,9 +236,5 @@ fn shallow_eq<'gc>(
} }
fn bool_outcome<'gc>(b: bool) -> ShallowEq<'gc> { fn bool_outcome<'gc>(b: bool) -> ShallowEq<'gc> {
if b { if b { ShallowEq::True } else { ShallowEq::False }
ShallowEq::True
} else {
ShallowEq::False
}
} }
@@ -1,12 +1,12 @@
use std::path::PathBuf; use std::path::PathBuf;
use fix_abstract_vm::{ use fix_bytecode::Continuation;
use fix_error::Error;
use fix_lang::StringId;
use fix_runtime::{
AttrSet, Break, BytecodeReader, CallFrame, Machine, MachineExt, Path, PendingLoad, AttrSet, Break, BytecodeReader, CallFrame, Machine, MachineExt, Path, PendingLoad,
PendingScope, Step, StrictValue, Value, VmRuntimeCtx, VmRuntimeCtxExt, canon_path_str, PendingScope, Step, StrictValue, Value, VmRuntimeCtx, VmRuntimeCtxExt, canon_path_str,
}; };
use fix_builtins::PrimOpPhase;
use fix_common::StringId;
use fix_error::Error;
use gc_arena::{Gc, Mutation}; use gc_arena::{Gc, Mutation};
use hashbrown::HashSet; use hashbrown::HashSet;
@@ -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: PrimOpPhase::ImportFinalize.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: PrimOpPhase::ScopedImportFinalize.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
+116
View File
@@ -0,0 +1,116 @@
use fix_error::Result;
use fix_macros::handler;
use fix_runtime::{List, Slot, StrictValue, Value};
use gc_arena::Mutation;
use crate::primops::stubs::*;
#[handler(name = PFilter)]
fn filter<'gc>(
mc: &Mutation<'gc>,
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(list.get()));
}
let idx: Slot<i32> = spill(0i32);
let acc: Slot<List<'gc>> = spill(List::new_gc(mc));
loop {
#[expect(
clippy::indexing_slicing,
reason = "idx is a non-negative loop counter in 0..list.len(), so it indexes the list in bounds"
)]
let keep: bool = call(&pred, list.get().inner.borrow()[idx.get() as usize]).await?;
if keep {
#[expect(
clippy::indexing_slicing,
reason = "idx is a non-negative loop counter in 0..list.len(), so it indexes the list in bounds"
)]
let elem = list.get().inner.borrow()[idx.get() as usize];
acc.get().unlock(mc).borrow_mut().push(elem);
}
if idx.get() as usize == list.get().inner.borrow().len() - 1 {
return Ok(Value::new(acc.get()));
}
idx.set(idx.get() + 1);
}
}
#[handler(name = PAll)]
fn all<'gc>(
mc: &Mutation<'gc>,
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));
}
let idx: Slot<i32> = spill(0i32);
loop {
#[expect(
clippy::indexing_slicing,
reason = "idx is a non-negative loop counter in 0..list.len(), so it indexes the list in bounds"
)]
let keep: bool = call(&pred, list.get().inner.borrow()[idx.get() as usize]).await?;
if !keep || idx.get() as usize + 1 == list.get().inner.borrow().len() {
return Ok(Value::new(keep));
}
idx.set(idx.get() + 1);
}
}
#[handler(name = PAny)]
fn any<'gc>(
mc: &Mutation<'gc>,
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(false));
}
let idx: Slot<i32> = spill(0i32);
loop {
#[expect(
clippy::indexing_slicing,
reason = "idx is a non-negative loop counter in 0..list.len(), so it indexes the list in bounds"
)]
let keep: bool = call(&pred, list.get().inner.borrow()[idx.get() as usize]).await?;
if keep || idx.get() as usize + 1 == list.get().inner.borrow().len() {
return Ok(Value::new(keep));
}
idx.set(idx.get() + 1);
}
}
#[handler(name = PFoldlStrict)]
fn foldl_strict<'gc>(
mc: &Mutation<'gc>,
op: Slot<Value<'gc>>,
nul: 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(nul.get());
}
let idx: Slot<i32> = spill(0i32);
let acc = spill(nul.get());
loop {
let f = call(&op, acc.get()).await?;
#[expect(
clippy::indexing_slicing,
reason = "idx is a non-negative loop counter in 0..list.len(), so it indexes the list in bounds"
)]
let new_acc: StrictValue<'gc> =
call(&f, list.get().inner.borrow()[idx.get() as usize]).await?;
acc.set(new_acc.relax());
if idx.get() as usize + 1 == list.get().inner.borrow().len() {
return Ok(acc.get());
}
idx.set(idx.get() + 1);
}
}
+88
View File
@@ -0,0 +1,88 @@
mod context;
mod control;
mod conv;
mod eq;
mod io;
mod list;
mod path;
mod stubs;
pub use context::*;
pub use control::*;
pub use conv::*;
pub use eq::*;
use fix_bytecode::Continuation;
use fix_error::Error;
use fix_runtime::{BytecodeReader, Machine, Step, VmRuntimeCtx};
use gc_arena::Mutation;
pub use io::*;
pub use list::*;
pub use path::*;
pub fn dispatch_cont<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
use Continuation::*;
let cont = reader.read_u8();
let Ok(cont) = Continuation::try_from(cont) else {
return m.finish_err(Error::eval_error("invalid primop phase"));
};
match cont {
PAbort => abort(m, ctx, reader, mc),
PAll0 | PAll1 | PAll2 | PAll3 => all(m, reader, mc, cont),
PAny0 | PAny1 | PAny2 | PAny3 => any(m, reader, mc, cont),
PDeepSeq0 | PDeepSeq1 | PDeepSeq2 | PDeepSeq3 | PDeepSeq4 | PDeepSeq5 => {
deep_seq(m, reader, mc, cont)
}
PSeq0 | PSeq1 => seq(m, reader, mc, cont),
PFilter0 | PFilter1 | PFilter2 | PFilter3 | PFilter4 => filter(m, reader, mc, cont),
PFoldlStrict0 | PFoldlStrict1 | PFoldlStrict2 | PFoldlStrict3 | PFoldlStrict4 | PFoldlStrict5 => {
foldl_strict(m, reader, mc, cont)
}
ForceResultShallow => force_result_shallow(m, ctx, reader, mc),
ForceResultShallowPush => force_result_shallow_push(m, ctx, reader, mc),
ForceResultShallowLoop => force_result_shallow_loop(m, reader, mc),
ForceResultDeepFinish => force_result_deep_finish(m, ctx, reader, mc),
EqStep => eq_step(m, reader, mc),
EqForce => eq_force(m, ctx, reader, mc),
CallPattern => call_pattern(m, ctx, reader, mc),
CallFunctor1 => call_functor_1(m, reader, mc),
CallFunctor2 => call_functor_2(m, reader, mc),
PImport => import(m, ctx, reader, mc),
PImportFinalize => import_finalize(m, ctx, reader),
PScopedImport => scoped_import(m, ctx, reader, mc),
PScopedImportFinalize => scoped_import_finalize(m, ctx, reader, mc),
PPathExists => path_exists(m, ctx, reader, mc),
PToPath => to_path(m, ctx, reader, mc),
PIsPath => is_path(m, reader, mc),
PToString => to_string(m, ctx, reader, mc),
PTypeOf => type_of(m, ctx, reader, mc),
PHasContext => has_context(m, ctx, reader, mc),
PGetContext => get_context(m, ctx, reader, mc),
PAppendContext => append_context(m, ctx, reader, mc),
PAppendContextLoop => append_context_loop(m, ctx, reader, mc),
PAppendContextEntryForced => append_context_entry_forced(m, ctx, reader, mc),
PAppendContextOutputsForced => append_context_outputs_forced(m, ctx, reader, mc),
PAppendContextOutputElementLoop => append_context_output_element_loop(m, ctx, reader, mc),
PAppendContextOutputElementForced => {
append_context_output_element_forced(m, ctx, reader, mc)
}
PUnsafeDiscardStringContext => unsafe_discard_string_context(m, ctx, reader, mc),
PUnsafeDiscardOutputDependency => unsafe_discard_output_dependency(m, ctx, reader, mc),
phase => todo!("primop phase {phase:?}"),
}
}
@@ -1,8 +1,8 @@
use fix_abstract_vm::{ use fix_error::Error;
use fix_runtime::{
BytecodeReader, Machine, MachineExt, Path, Step, StrictValue, Value, VmRuntimeCtx, BytecodeReader, Machine, MachineExt, Path, Step, StrictValue, Value, VmRuntimeCtx,
VmRuntimeCtxExt, canon_path_str, VmRuntimeCtxExt, canon_path_str,
}; };
use fix_error::Error;
use gc_arena::Mutation; use gc_arena::Mutation;
pub fn to_path<'gc, M: Machine<'gc>>( pub fn to_path<'gc, M: Machine<'gc>>(
@@ -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};
+20 -23
View File
@@ -3,6 +3,18 @@ name = "fix"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
[[bench]]
harness = false
name = "basic_ops"
[[bench]]
harness = false
name = "builtins"
[[bench]]
harness = false
name = "thunk_scope"
[dependencies] [dependencies]
mimalloc = "0.1" mimalloc = "0.1"
@@ -17,43 +29,28 @@ clap = { version = "4", features = ["derive"] }
tracing = "0.1" tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] } tracing-subscriber = { version = "0.3", features = ["env-filter"] }
miette = { version = "7.4", features = ["fancy"] }
# Error Reporting # Error Reporting
thiserror = "2" thiserror = "2"
miette = { version = "7.4", features = ["fancy"] }
# Data Structure # Data Structure
hashbrown = { workspace = true } hashbrown = { workspace = true }
string-interner = { workspace = true } string-interner = { workspace = true }
# Memory Management
bumpalo = { workspace = true }
rnix = { workspace = true }
ere = { workspace = true } ere = { workspace = true }
ghost-cell = { workspace = true }
fix-abstract-vm = { path = "../fix-abstract-vm" } fix-bytecode = { path = "../fix-bytecode" }
fix-builtins = { path = "../fix-builtins" } fix-compiler = { path = "../fix-compiler" }
fix-common = { path = "../fix-common" }
fix-codegen = { path = "../fix-codegen" }
fix-error = { path = "../fix-error" } fix-error = { path = "../fix-error" }
fix-ir = { path = "../fix-ir" } fix-lang = { path = "../fix-lang" }
fix-runtime = { path = "../fix-runtime" }
fix-vm = { path = "../fix-vm" } 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"] }
[[bench]] [lints]
name = "basic_ops" workspace = true
harness = false
[[bench]]
name = "builtins"
harness = false
[[bench]]
name = "thunk_scope"
harness = false
+3 -2
View File
@@ -1,8 +1,9 @@
#![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_common::Value;
use fix_error::{Result, Source}; use fix_error::{Result, Source};
use fix_lang::Value;
pub fn eval(expr: &str) -> Value { pub fn eval(expr: &str) -> Value {
Evaluator::new() Evaluator::new()
+25 -600
View File
@@ -1,24 +1,13 @@
#![warn(clippy::unwrap_used)] use fix_bytecode::InstructionPtr;
#![allow(dead_code)] use fix_bytecode::disassembler::{Disassembler, DisassemblerContext};
use fix_compiler::{CodeState, ExtraScope};
use bumpalo::Bump; use fix_error::{Result, Source};
use fix_abstract_vm::{ForceMode, StaticValue, VmCode, VmContext, VmRuntimeCtx}; use fix_lang::StringId;
use fix_builtins::PrimOpPhase; use fix_runtime::{ForceMode, StaticValue, VmCode, VmContext, VmRuntimeCtx};
use fix_codegen::disassembler::{Disassembler, DisassemblerContext};
use fix_codegen::{BytecodeContext, InstructionPtr, Op};
use fix_common::{StringId, Symbol};
use fix_error::{Error, Result, Source};
use fix_ir::downgrade::{Downgrade as _, DowngradeContext};
use fix_ir::{
GhostMaybeThunkRef, GhostRoIrRef, GhostRoMaybeThunkRef, GhostRoRef, Ir, MaybeThunk, RawIrRef,
ThunkId,
};
use fix_vm::Vm; use fix_vm::Vm;
use ghost_cell::{GhostCell, GhostToken};
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]
@@ -29,15 +18,6 @@ pub struct RuntimeState {
pub constants: Constants, pub constants: Constants,
} }
pub struct CodeState {
pub bytecode: Vec<u8>,
pub sources: Vec<Source>,
pub spans: Vec<(usize, rnix::TextRange)>,
pub thunk_count: usize,
pub global_env: HashMap<StringId, MaybeThunk>,
pub current_scope_slot: Option<u32>,
}
pub struct Evaluator { pub struct Evaluator {
pub runtime: RuntimeState, pub runtime: RuntimeState,
pub code: CodeState, pub code: CodeState,
@@ -52,37 +32,25 @@ impl Default for Evaluator {
impl Evaluator { impl Evaluator {
pub fn new() -> Self { pub fn new() -> Self {
let mut strings = DefaultStringInterner::new(); let mut strings = DefaultStringInterner::new();
let global_env = fix_ir::new_global_env(&mut strings); let code = CodeState::new(&mut strings);
let mut bytecode = Vec::with_capacity(PrimOpPhase::Illegal as usize * 2);
for phase in 0..=PrimOpPhase::Illegal as u8 {
bytecode.push(Op::DispatchPrimOp as u8);
bytecode.push(phase);
}
Self { Self {
runtime: RuntimeState { runtime: RuntimeState {
strings, strings,
constants: Constants::default(), constants: Constants::default(),
}, },
code: CodeState { code,
sources: Vec::new(),
spans: Vec::new(),
thunk_count: 0,
bytecode,
global_env,
current_scope_slot: None,
},
} }
} }
pub fn eval(&mut self, source: Source) -> Result<fix_common::Value> { pub fn eval(&mut self, source: Source) -> Result<fix_lang::Value> {
self.do_eval(source, None, ForceMode::AsIs) self.do_eval(source, None, ForceMode::AsIs)
} }
pub fn eval_shallow(&mut self, source: Source) -> Result<fix_common::Value> { pub fn eval_shallow(&mut self, source: Source) -> Result<fix_lang::Value> {
self.do_eval(source, None, ForceMode::Shallow) self.do_eval(source, None, ForceMode::Shallow)
} }
pub fn eval_deep(&mut self, source: Source) -> Result<fix_common::Value> { pub fn eval_deep(&mut self, source: Source) -> Result<fix_lang::Value> {
self.do_eval(source, None, ForceMode::Deep) self.do_eval(source, None, ForceMode::Deep)
} }
@@ -90,7 +58,7 @@ impl Evaluator {
&mut self, &mut self,
source: Source, source: Source,
scope: &HashSet<StringId>, scope: &HashSet<StringId>,
) -> Result<fix_common::Value> { ) -> Result<fix_lang::Value> {
self.do_eval(source, Some(ExtraScope::Repl(scope)), ForceMode::Shallow) self.do_eval(source, Some(ExtraScope::Repl(scope)), ForceMode::Shallow)
} }
@@ -99,14 +67,10 @@ impl Evaluator {
source: Source, source: Source,
extra_scope: Option<ExtraScope<'ctx>>, extra_scope: Option<ExtraScope<'ctx>>,
force_mode: ForceMode, force_mode: ForceMode,
) -> Result<fix_common::Value> { ) -> Result<fix_lang::Value> {
let ip = { let ip = self
let mut compiler = CompilerCtx { .code
code: &mut self.code, .compile_bytecode(source, extra_scope, &mut self.runtime)?;
runtime: &mut self.runtime,
};
compiler.compile_bytecode(source, extra_scope)?
};
Vm::run(self, ip, force_mode) Vm::run(self, ip, force_mode)
} }
@@ -115,16 +79,12 @@ impl Evaluator {
_ident: &str, _ident: &str,
_expr: &str, _expr: &str,
_scope: &mut HashSet<StringId>, _scope: &mut HashSet<StringId>,
) -> Result<fix_common::Value> { ) -> Result<fix_lang::Value> {
todo!("add_binding") todo!("add_binding")
} }
pub fn compile_bytecode(&mut self, source: Source) -> Result<InstructionPtr> { pub fn compile_bytecode(&mut self, source: Source) -> Result<InstructionPtr> {
let mut compiler = CompilerCtx { self.code.compile_bytecode(source, None, &mut self.runtime)
code: &mut self.code,
runtime: &mut self.runtime,
};
compiler.compile_bytecode(source, None)
} }
pub fn disassemble_colored(&self, ip: InstructionPtr) -> String { pub fn disassemble_colored(&self, ip: InstructionPtr) -> String {
@@ -137,163 +97,24 @@ 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)
} }
} }
impl VmCode for CodeState {
fn bytecode(&self) -> &[u8] {
&self.bytecode
}
fn compile_with_scope(
&mut self,
source: Source,
extra_scope: Option<fix_abstract_vm::ExtraScope>,
runtime: &mut impl VmRuntimeCtx,
) -> Result<InstructionPtr> {
let mut compiler = CompilerCtx {
code: self,
runtime,
};
let extra = extra_scope.map(|s| match s {
fix_abstract_vm::ExtraScope::ScopedImport { keys, slot_id } => {
ExtraScope::ScopedImport { keys, slot_id }
}
});
compiler.compile_bytecode(source, extra)
}
}
impl VmContext for Evaluator { impl VmContext for Evaluator {
fn split(&mut self) -> (&mut impl VmCode, &mut impl VmRuntimeCtx) { fn split(&mut self) -> (&mut impl VmCode, &mut impl VmRuntimeCtx) {
(&mut self.code, &mut self.runtime) (&mut self.code, &mut self.runtime)
} }
} }
struct CompilerCtx<'a, R: VmRuntimeCtx> {
code: &'a mut CodeState,
runtime: &'a mut R,
}
impl<'a, R: VmRuntimeCtx> CompilerCtx<'a, R> {
fn compile_bytecode(
&mut self,
source: Source,
extra_scope: Option<ExtraScope>,
) -> Result<InstructionPtr> {
let prev_scope_slot = self.code.current_scope_slot;
self.code.current_scope_slot = match &extra_scope {
Some(ExtraScope::ScopedImport { slot_id, .. }) => Some(*slot_id),
_ => None,
};
let result = (|| -> Result<InstructionPtr> {
let root = self.downgrade(source, extra_scope)?;
let ip = fix_codegen::compile_bytecode(root.as_ref(), self);
Ok(ip)
})();
self.code.current_scope_slot = prev_scope_slot;
result
}
fn downgrade(&mut self, source: Source, extra_scope: Option<ExtraScope>) -> Result<OwnedIr> {
tracing::debug!("Parsing Nix expression");
self.code.sources.push(source.clone());
let root = rnix::Root::parse(&source.src);
handle_parse_error(root.errors(), source.clone()).map_or(Ok(()), Err)?;
tracing::debug!("Downgrading Nix expression");
let expr = root
.tree()
.expr()
.ok_or_else(|| Error::parse_error("unexpected EOF".into()))?;
let bump = Bump::new();
GhostToken::new(|token| {
let downgrade_ctx = DowngradeCtx::new(
&bump,
token,
self.runtime,
&self.code.global_env,
extra_scope.map(Into::into),
&mut self.code.thunk_count,
source,
);
let ir = downgrade_ctx.downgrade_toplevel(expr)?;
let ir = unsafe { std::mem::transmute::<RawIrRef<'_>, RawIrRef<'static>>(ir) };
Ok(OwnedIr { _bump: bump, ir })
})
}
}
impl<'a, R: VmRuntimeCtx> BytecodeContext for CompilerCtx<'a, R> {
fn intern_string(&mut self, s: &str) -> StringId {
self.runtime.intern_string(s)
}
fn register_span(&mut self, range: rnix::TextRange) -> u32 {
let id = self.code.spans.len();
let source_id = self
.code
.sources
.len()
.checked_sub(1)
.expect("current_source not set");
self.code.spans.push((source_id, range));
id as u32
}
fn get_code(&self) -> &[u8] {
&self.code.bytecode
}
fn get_code_mut(&mut self) -> &mut Vec<u8> {
&mut self.code.bytecode
}
fn add_constant(&mut self, val: fix_codegen::Const) -> u32 {
use fix_codegen::Const::*;
let val = match val {
Smi(x) => StaticValue::new_inline(x),
Float(x) => StaticValue::new_float(x),
Bool(x) => StaticValue::new_inline(x),
String(x) => StaticValue::new_inline(x),
Path(_) => todo!("path value type"),
PrimOp {
id,
arity,
dispatch_ip,
} => StaticValue::new_primop(id, arity, dispatch_ip),
Null => StaticValue::default(),
};
self.runtime.add_const(val)
}
fn current_source_dir(&mut self) -> StringId {
let dir = self
.code
.sources
.last()
.expect("current_source not set")
.get_dir()
.to_string_lossy()
.into_owned();
self.runtime.intern_string(dir)
}
fn current_scope_slot(&self) -> Option<u32> {
self.code.current_scope_slot
}
}
#[derive(Default)] #[derive(Default)]
pub struct Constants { pub struct Constants {
data: Vec<StaticValue>, data: Vec<StaticValue>,
@@ -315,410 +136,14 @@ impl Constants {
} }
} }
fn parse_error_span(error: &rnix::ParseError) -> Option<rnix::TextRange> {
use rnix::ParseError::*;
match error {
Unexpected(range)
| UnexpectedExtra(range)
| UnexpectedWanted(_, range, _)
| UnexpectedDoubleBind(range)
| DuplicatedArgs(range, _) => Some(*range),
_ => None,
}
}
fn handle_parse_error<'a>(
errors: impl IntoIterator<Item = &'a rnix::ParseError>,
source: Source,
) -> Option<Box<Error>> {
for err in errors {
if let Some(span) = parse_error_span(err) {
return Some(
Error::parse_error(err.to_string())
.with_source(source)
.with_span(span),
);
}
}
None
}
struct DowngradeCtx<'ctx, 'id, 'ir, R: VmRuntimeCtx> {
bump: &'ir Bump,
token: GhostToken<'id>,
runtime: &'ctx mut R,
source: Source,
scopes: Vec<Scope<'ctx, 'id, 'ir>>,
with_stack: Vec<GhostRoMaybeThunkRef<'id, 'ir>>,
arg_count: u32,
thunk_count: &'ctx mut usize,
thunk_scopes: Vec<ThunkScope<'id, 'ir>>,
}
impl<'ctx, 'id, 'ir, R: VmRuntimeCtx> DowngradeCtx<'ctx, 'id, 'ir, R> {
fn new(
bump: &'ir Bump,
token: GhostToken<'id>,
runtime: &'ctx mut R,
global: &'ctx HashMap<StringId, MaybeThunk>,
extra_scope: Option<Scope<'ctx, 'id, 'ir>>,
thunk_count: &'ctx mut usize,
source: Source,
) -> Self {
Self {
bump,
token,
runtime,
source,
scopes: std::iter::once(Scope::Global(global))
.chain(extra_scope)
.collect(),
thunk_count,
arg_count: 0,
with_stack: Vec::new(),
thunk_scopes: vec![ThunkScope::new_in(bump)],
}
}
}
impl<'ctx: 'ir, 'id, 'ir, R: VmRuntimeCtx> DowngradeContext<'id, 'ir>
for DowngradeCtx<'ctx, 'id, 'ir, R>
{
fn new_expr(&self, expr: Ir<'ir, GhostRoRef<'id, 'ir>>) -> GhostRoIrRef<'id, 'ir> {
self.bump.alloc(GhostCell::new(expr).into())
}
fn maybe_thunk(&mut self, ir: GhostRoIrRef<'id, 'ir>) -> GhostRoMaybeThunkRef<'id, 'ir> {
use MaybeThunk::*;
let expr = (|| {
let expr = match *ir.borrow(&self.token) {
Ir::Builtin(x) => Builtin(x),
Ir::Int(x) => Int(x),
Ir::Float(x) => Float(x),
Ir::Bool(x) => Bool(x),
Ir::Str(x) => Str(x),
Ir::Arg { layer } => Arg { layer },
Ir::Builtins => Builtins,
Ir::Null => Null,
Ir::MaybeThunk(thunk) => return Some(thunk),
_ => return None,
};
Some(self.bump.alloc(GhostCell::new(expr).into()))
})();
if let Some(thunk) = expr {
return thunk;
}
let id = ThunkId(*self.thunk_count);
*self.thunk_count = self.thunk_count.checked_add(1).expect("thunk id overflow");
self.thunk_scopes
.last_mut()
.expect("no active cache scope")
.add_binding(id, ir);
self.bump.alloc(GhostCell::new(Thunk(id)).into())
}
fn intern_string(&mut self, sym: impl AsRef<str>) -> StringId {
self.runtime.intern_string(sym)
}
fn resolve_sym(&self, id: StringId) -> Symbol<'_> {
self.runtime.resolve_string(id).into()
}
fn lookup(
&mut self,
sym: StringId,
span: rnix::TextRange,
) -> Result<GhostRoMaybeThunkRef<'id, 'ir>> {
for scope in self.scopes.iter().rev() {
match scope {
&Scope::Global(global_scope) => {
if let Some(expr) = global_scope.get(&sym) {
return Ok(expr.into());
}
}
&Scope::Repl(repl_bindings) => {
if repl_bindings.contains(&sym) {
return Ok(self
.bump
.alloc(GhostCell::new(MaybeThunk::ReplBinding(sym)).into()));
}
}
Scope::ScopedImport { keys, .. } => {
if keys.contains(&sym) {
return Ok(self
.bump
.alloc(GhostCell::new(MaybeThunk::ScopedImportBinding(sym)).into()));
}
}
Scope::Let(let_scope) => {
if let Some(&expr) = let_scope.get(&sym) {
return Ok(expr.into());
}
}
&Scope::Param {
sym: param_sym,
abs_layer,
} => {
if param_sym == sym {
let layers: u8 =
self.thunk_scopes.len().try_into().expect("scope too deep!");
let layer = layers - abs_layer;
return Ok(self
.bump
.alloc(GhostCell::new(MaybeThunk::Arg { layer }).into()));
}
}
}
}
if !self.with_stack.is_empty() {
let id = ThunkId(*self.thunk_count);
*self.thunk_count = self.thunk_count.checked_add(1).expect("thunk id overflow");
let mut namespaces =
bumpalo::collections::Vec::with_capacity_in(self.with_stack.len(), self.bump);
namespaces.extend(self.with_stack.iter().rev().copied());
let body = self
.bump
.alloc(GhostCell::new(Ir::WithLookup { sym, namespaces }).into());
self.thunk_scopes
.last_mut()
.expect("no active thunk scope")
.add_binding(id, body);
Ok(self
.bump
.alloc(GhostCell::new(MaybeThunk::Thunk(id)).into()))
} else {
Err(Error::downgrade_error(
format!("'{}' not found", self.resolve_sym(sym)),
self.get_current_source(),
span,
))
}
}
fn get_current_source(&self) -> Source {
self.source.clone()
}
fn with_let_scope<F, Ret>(&mut self, keys: &[StringId], f: F) -> Result<Ret>
where
F: FnOnce(
&mut Self,
) -> Result<(
bumpalo::collections::Vec<'ir, GhostRoMaybeThunkRef<'id, 'ir>>,
Ret,
)>,
{
let base = *self.thunk_count;
*self.thunk_count = self
.thunk_count
.checked_add(keys.len())
.expect("thunk id overflow");
let handles = (base..base + keys.len())
.map(|id| {
&*self
.bump
.alloc(GhostCell::new(MaybeThunk::Thunk(ThunkId(id))))
})
.collect::<Vec<_>>();
let scope = keys.iter().copied().zip(handles.iter().copied()).collect();
self.scopes.push(Scope::Let(scope));
let (vals, ret) = { f(self)? };
self.scopes.pop();
assert_eq!(keys.len(), vals.len());
let scope = self.thunk_scopes.last_mut().expect("no active thunk scope");
for (i, (val, handle)) in vals.into_iter().zip(handles).enumerate() {
let thunk = *val.borrow(&self.token);
*handle.borrow_mut(&mut self.token) = thunk;
let id = ThunkId(base + i);
let ir_ref = self
.bump
.alloc(GhostCell::new(Ir::MaybeThunk(handle.into())).into());
scope.add_binding(id, ir_ref);
}
Ok(ret)
}
fn with_param_scope<F, Ret>(&mut self, sym: StringId, f: F) -> Ret
where
F: FnOnce(&mut Self) -> Ret,
{
self.scopes.push(Scope::Param {
sym,
abs_layer: self.thunk_scopes.len().try_into().expect("scope too deep!"),
});
let mut guard = ScopeGuard { ctx: self };
f(guard.as_ctx())
}
fn with_with_scope<F, Ret>(&mut self, namespace: GhostRoMaybeThunkRef<'id, 'ir>, f: F) -> Ret
where
F: FnOnce(&mut Self) -> Ret,
{
self.with_stack.push(namespace);
let ret = f(self);
self.with_stack.pop();
ret
}
fn with_thunk_scope<F, Ret>(
&mut self,
f: F,
) -> (
Ret,
bumpalo::collections::Vec<'ir, (ThunkId, GhostRoIrRef<'id, 'ir>)>,
)
where
F: FnOnce(&mut Self) -> Ret,
{
if self.thunk_scopes.len() == u8::MAX as usize {
panic!("scope too deep!");
}
self.thunk_scopes.push(ThunkScope::new_in(self.bump));
let ret = f(self);
(
ret,
self.thunk_scopes
.pop()
.expect("no thunk scope left???")
.bindings,
)
}
fn bump(&self) -> &'ir bumpalo::Bump {
self.bump
}
}
impl<'id, 'ir, 'ctx: 'ir, R: VmRuntimeCtx> DowngradeCtx<'ctx, 'id, 'ir, R> {
fn downgrade_toplevel(mut self, root: rnix::ast::Expr) -> Result<RawIrRef<'ir>> {
let body = root.downgrade(&mut self)?;
let thunks = self
.thunk_scopes
.pop()
.expect("no thunk scope left???")
.bindings;
Ok(Ir::freeze(
self.new_expr(Ir::TopLevel { body, thunks }),
self.token,
))
}
}
struct ThunkScope<'id, 'ir> {
bindings: bumpalo::collections::Vec<'ir, (ThunkId, GhostRoIrRef<'id, 'ir>)>,
}
impl<'id, 'ir> ThunkScope<'id, 'ir> {
fn new_in(bump: &'ir Bump) -> Self {
Self {
bindings: bumpalo::collections::Vec::new_in(bump),
}
}
fn add_binding(&mut self, id: ThunkId, ir: GhostRoIrRef<'id, 'ir>) {
self.bindings.push((id, ir));
}
fn extend_bindings(
&mut self,
iter: impl IntoIterator<Item = (ThunkId, GhostRoIrRef<'id, 'ir>)>,
) {
self.bindings.extend(iter);
}
}
enum Scope<'ctx, 'id, 'ir> {
Global(&'ctx HashMap<StringId, MaybeThunk>),
Repl(&'ctx HashSet<StringId>),
ScopedImport {
keys: HashSet<StringId>,
slot_id: u32,
},
Let(HashMap<StringId, GhostMaybeThunkRef<'id, 'ir>>),
Param {
sym: StringId,
abs_layer: u8,
},
}
pub enum ExtraScope<'ctx> {
Repl(&'ctx HashSet<StringId>),
ScopedImport {
keys: HashSet<StringId>,
slot_id: u32,
},
}
impl<'ctx> From<ExtraScope<'ctx>> for Scope<'ctx, '_, '_> {
fn from(value: ExtraScope<'ctx>) -> Self {
use ExtraScope::*;
match value {
ScopedImport { keys, slot_id } => Scope::ScopedImport { keys, slot_id },
Repl(scope) => Scope::Repl(scope),
}
}
}
struct ScopeGuard<'a, 'ctx, 'id, 'ir, R: VmRuntimeCtx> {
ctx: &'a mut DowngradeCtx<'ctx, 'id, 'ir, R>,
}
impl<'a, 'ctx, 'id, 'ir, R: VmRuntimeCtx> Drop for ScopeGuard<'a, 'ctx, 'id, 'ir, R> {
fn drop(&mut self) {
self.ctx.scopes.pop();
}
}
impl<'a, 'ctx, 'id, 'ir, R: VmRuntimeCtx> ScopeGuard<'a, 'ctx, 'id, 'ir, R> {
fn as_ctx(&mut self) -> &mut DowngradeCtx<'ctx, 'id, 'ir, R> {
self.ctx
}
}
struct OwnedIr {
_bump: Bump,
ir: RawIrRef<'static>,
}
impl OwnedIr {
/// # Safety
/// `ir` must be an allocation backed by `bump`. The reference's
/// lifetime is extended to `'static` as a placeholder; the stored IR
/// must only be re-borrowed via [`OwnedIr::as_ref`], which narrows
/// the lifetime back to that of the `&self` borrow. Moving `bump`
/// into the struct keeps the underlying allocation live for the
/// lifetime of the `OwnedIr`.
unsafe fn new(ir: RawIrRef<'_>, bump: Bump) -> Self {
Self {
_bump: bump,
// SAFETY: see function docs - caller guarantees `ir` is in `bump`,
// and the `'static` lifetime is a placeholder narrowed by `as_ref`.
ir: unsafe { std::mem::transmute::<RawIrRef<'_>, RawIrRef<'static>>(ir) },
}
}
fn as_ref<'ir>(&'ir self) -> RawIrRef<'ir> {
// SAFETY: narrows the placeholder `'static` lifetime stored in
// `self.ir` down to `'ir = &'ir self`. Lifetime shortening is
// logically sound for covariant positions; the transmute is only
// needed because `RawRef<'ir>` carries `'ir` through a GAT
// (`Ref::Ref<T>`), which prevents the compiler from inferring
// covariance automatically. The bump arena that backs the IR is
// owned by `self._bump`, so the data is live for at least `'ir`.
unsafe { std::mem::transmute::<RawIrRef<'static>, RawIrRef<'ir>>(self.ir) }
}
}
impl DisassemblerContext for Evaluator { impl DisassemblerContext for Evaluator {
fn get_code(&self) -> &[u8] { fn get_code(&self) -> &[u8] {
&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),
} }
} }
+2 -4
View File
@@ -1,4 +1,4 @@
use fix_common::Value; use fix_lang::Value;
use crate::utils::{eval_deep, eval_deep_result}; use crate::utils::{eval_deep, eval_deep_result};
@@ -402,7 +402,6 @@ 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";
@@ -411,8 +410,7 @@ fn fixed_output_missing_hashalgo() {
outputHash = "0000000000000000000000000000000000000000000000000000000000000000"; outputHash = "0000000000000000000000000000000000000000000000000000000000000000";
}"#, }"#,
) )
.is_err() .unwrap_err();
);
} }
#[test_log::test] #[test_log::test]
+2 -2
View File
@@ -1,6 +1,6 @@
use fix::Evaluator; use fix::Evaluator;
use fix_common::Value;
use fix_error::Source; use fix_error::Source;
use fix_lang::Value;
use crate::utils::{eval, eval_result}; use crate::utils::{eval, eval_result};
@@ -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]
+16 -5
View File
@@ -3,8 +3,9 @@
use std::path::PathBuf; use std::path::PathBuf;
use fix::Evaluator; use fix::Evaluator;
use fix_common::Value;
use fix_error::{Source, SourceType}; use fix_error::{Source, SourceType};
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!(
#[serial(env)]
getenv,
|| {
// SAFETY: guarded with #[serial_test::serial]
unsafe { std::env::set_var("TEST_VAR", "foo") }; 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!(
#[serial(env)]
path_string_interpolation,
|| {
// SAFETY: guarded with #[serial_test::serial]
unsafe { unsafe {
std::env::set_var("HOME", "/fake-home"); 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;
+9 -9
View File
@@ -1,5 +1,5 @@
use fix::Evaluator; use fix::Evaluator;
use fix_common::Value; use fix_lang::Value;
use crate::utils::eval_result; use crate::utils::eval_result;
@@ -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#"
+1 -3
View File
@@ -1,8 +1,6 @@
#![allow(dead_code)]
use fix::Evaluator; use fix::Evaluator;
use fix_common::Value;
use fix_error::{Result, Source}; use fix_error::{Result, Source};
use fix_lang::Value;
pub fn eval(expr: &str) -> Value { pub fn eval(expr: &str) -> Value {
Evaluator::new() Evaluator::new()
Generated
+9 -151
View File
@@ -1,64 +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": 1778445566,
"narHash": "sha256-oQvcadh2BCkrog+SGrG6YffKJrveYpjj3TdQJWaKhaM=",
"owner": "nix-community",
"repo": "bun2nix",
"rev": "2499dedd70744dba1815875b854818a3019e9e4c",
"type": "github"
},
"original": {
"owner": "nix-community",
"ref": "staging-2.1.0",
"repo": "bun2nix",
"type": "github"
}
},
"fenix": { "fenix": {
"inputs": { "inputs": {
"nixpkgs": [ "nixpkgs": [
@@ -67,11 +8,11 @@
"rust-analyzer-src": "rust-analyzer-src" "rust-analyzer-src": "rust-analyzer-src"
}, },
"locked": { "locked": {
"lastModified": 1778919578, "lastModified": 1784017020,
"narHash": "sha256-+z+jgTly48gsAiX8rOe/vs8C/2G4vdCpcEtqMJUpFqw=", "narHash": "sha256-49WO85egjNtN1vMgJ3zUjDh5IqO+ou4zZY80WQ6EKGg=",
"owner": "nix-community", "owner": "nix-community",
"repo": "fenix", "repo": "fenix",
"rev": "ecd6d4ff22cfdb1339b2915455a2ff4dc85bf52e", "rev": "fa2a0be0f712d7147d1677d33d15ea7b0589cdb4",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -94,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": 1778929997,
"narHash": "sha256-iAfbBUHBbR0N4DFqFWr4Jtmpc1YcOK7kpVM4f0MK1V8=",
"owner": "numtide",
"repo": "llm-agents.nix",
"rev": "0da8f0313c9f68c155e0932f880fc1913e7be846",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "llm-agents.nix",
"type": "github"
}
},
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1778443072, "lastModified": 1784007870,
"narHash": "sha256-zi7/fsqM/kFdNuED//4WOCUtezGtKKqRNORjMvfwjnA=", "narHash": "sha256-djcLt/JJphyNt4eDY9XTly+/WbCK5lqWq9lSgCmJkkQ=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "da5ad661ba4e5ef59ba743f0d112cbc30e474f32", "rev": "18b9261cb3294b6d2a06d03f96872827b8fe2698",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -160,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": 1778854817, "lastModified": 1783975201,
"narHash": "sha256-iG+VuMy8W585geVVCUd7pR025WsY3ZkgSv5Yt5bxDmQ=", "narHash": "sha256-oyHWTfKWk06nTynCO8ByEAx/KEARvOOyTVnwinOmK3Q=",
"owner": "rust-lang", "owner": "rust-lang",
"repo": "rust-analyzer", "repo": "rust-analyzer",
"rev": "1a68212c5683555ad80f0eab71db9715c6d52145", "rev": "63a6f0d4bcfd3bbcf36383fcbcbcd93456ed1653",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -180,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": 1775636079,
"narHash": "sha256-pc20NRoMdiar8oPQceQT47UUZMBTiMdUuWrYu2obUP0=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "790751ff7fd3801feeaf96d7dc416a8d581265ba",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "treefmt-nix",
"type": "github"
}
} }
}, },
"root": "root", "root": "root",
+3 -32
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,36 +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
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