Compare commits

..
1 Commits
Author SHA1 Message Date
imxyy1soope1 e4c65d43ad ForceMode (WIP) 2026-04-03 22:01:06 +08:00
85 changed files with 8130 additions and 11557 deletions
-1
View File
@@ -10,4 +10,3 @@ prof.json
*.cpuprofile *.cpuprofile
*.cpuprofile.gz *.cpuprofile.gz
*v8.log* *v8.log*
callgrind.*
-3
View File
@@ -16,9 +16,6 @@ vim.lsp.config("rust_analyzer", {
settings = { settings = {
["rust-analyzer"] = { ["rust-analyzer"] = {
cargo = { cargo = {
features = {
"tailcall"
}
} }
} }
} }
Generated
+2384 -514
View File
File diff suppressed because it is too large Load Diff
+1 -95
View File
@@ -1,103 +1,9 @@
[workspace] [workspace]
resolver = "3" resolver = "3"
members = [ members = [
"fix", "fix",
"fix-bytecode",
"fix-compiler",
"fix-error",
"fix-lang",
"fix-macros",
"fix-runtime",
"fix-vm",
] ]
[workspace.dependencies]
bumpalo = {
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]
inherits = "release"
lto = true
[profile.profiling] [profile.profiling]
inherits = "release" inherits = "release"
debug = true debug = true
+14 -3
View File
@@ -15,6 +15,17 @@
cargo run --release -- eval --expr '{{expr}}' cargo run --release -- eval --expr '{{expr}}'
[no-exit-message] [no-exit-message]
[positional-arguments] @repli:
@cg *args='': cargo run --release --features inspector -- --inspect-brk 127.0.0.1:9229 repl
valgrind --tool=callgrind --dump-instr=yes --collect-jumps=yes "$@"
[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}}'
+70
View File
@@ -0,0 +1,70 @@
{
"$schema": "https://biomejs.dev/schemas/2.3.14/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true
},
"files": {
"includes": ["**", "!!**/dist"]
},
"formatter": {
"enabled": true,
"formatWithErrors": true,
"attributePosition": "auto",
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 110,
"lineEnding": "lf"
},
"linter": {
"rules": {
"style": {
"useNamingConvention": {
"level": "warn",
"options": {
"strictCase": false,
"conventions": [
{
"selector": { "kind": "objectLiteralProperty" },
"formats": ["camelCase", "PascalCase", "CONSTANT_CASE"]
},
{
"selector": { "kind": "typeProperty" },
"formats": ["camelCase", "snake_case"]
}
]
}
}
}
}
},
"overrides": [
{
"includes": ["**/global.d.ts"],
"linter": {
"rules": {
"style": {
"useNamingConvention": "off"
}
}
}
}
],
"javascript": {
"formatter": {
"arrowParentheses": "always",
"bracketSameLine": false,
"bracketSpacing": true,
"jsxQuoteStyle": "double",
"quoteProperties": "asNeeded",
"semicolons": "always",
"trailingCommas": "all"
}
},
"json": {
"formatter": {
"trailingCommas": "none"
}
}
}
-5
View File
@@ -1,5 +0,0 @@
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
@@ -1,24 +0,0 @@
{ 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
];
}
-14
View File
@@ -1,14 +0,0 @@
[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
-392
View File
@@ -1,392 +0,0 @@
use std::fmt::Write;
use colored::Colorize as _;
use crate::{BytecodeReader, Continuation, InstructionPtr, Op};
pub trait DisassemblerContext {
fn resolve_string(&self, id: u32) -> &str;
fn get_code(&self) -> &[u8];
}
pub struct Disassembler<'a, Ctx> {
reader: BytecodeReader<'a>,
ctx: &'a Ctx,
}
impl<'a, Ctx: DisassemblerContext> Disassembler<'a, Ctx> {
pub fn new(ip: InstructionPtr, ctx: &'a Ctx) -> Self {
Self {
reader: BytecodeReader::new(ctx.get_code(), ip.0),
ctx,
}
}
pub fn disassemble(&mut self) -> String {
self.disassemble_impl(false)
}
pub fn disassemble_colored(&mut self) -> String {
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 {
let mut out = String::new();
if color {
let _ = writeln!(out, "{}", "=== Bytecode Disassembly ===".bold().white());
let _ = writeln!(
out,
"{} {}",
"Length:".white(),
format!("{} bytes", self.reader.len()).cyan()
);
} else {
let _ = writeln!(out, "=== Bytecode Disassembly ===");
let _ = writeln!(out, "Length: {} bytes", self.reader.len());
}
while self.reader.pc() < self.reader.len() {
let start_pos = self.reader.pc();
let op_byte = self.reader.read_u8();
let (mnemonic, args) = self.decode_instruction(op_byte, start_pos);
let bytes_slice = &self.reader[start_pos + 1..self.reader.pc()];
let mut chunks = bytes_slice.chunks(4);
let first_chunk = chunks.next().unwrap_or(&[]);
let bytes_str = {
let mut temp = format!("{:02x}", self.reader[start_pos]);
for b in first_chunk {
let _ = write!(&mut temp, " {:02x}", b);
}
temp
};
if color {
let sep = if args.is_empty() { "" } else { " " };
let _ = writeln!(
out,
"{} {:<14} | {}{}{}",
format!("{:04x}", start_pos).dimmed(),
bytes_str.green(),
mnemonic.yellow().bold(),
sep,
args.cyan()
);
} else {
let op_str = if args.is_empty() {
mnemonic.to_string()
} else {
format!("{} {}", mnemonic, args)
};
let _ = writeln!(out, "{:04x} {:<14} | {}", start_pos, bytes_str, op_str);
}
for chunk in chunks {
let bytes_str = {
let mut temp = String::from(" ");
for b in chunk {
let _ = write!(&mut temp, " {:02x}", b);
}
temp
};
let extra_width = if start_pos > 0 {
start_pos.ilog2() >> 4
} else {
0
};
if color {
let _ = write!(out, " ");
for _ in 0..extra_width {
let _ = write!(out, " ");
}
let _ = writeln!(out, " {:<14} |", bytes_str.green());
} else {
let _ = write!(out, " ");
for _ in 0..extra_width {
let _ = write!(out, " ");
}
let _ = writeln!(out, " {:<14} |", bytes_str);
}
}
}
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) {
let op = Op::try_from(op_byte).expect("invalid op code");
match op {
Op::PushSmi => {
let val = self.reader.read_i32();
("PushSmi", format!("{}", val))
}
Op::PushBigInt => {
let val = self.reader.read_i64();
("PushBigInt", format!("{}", val))
}
Op::PushFloat => {
let val = self.reader.read_f64();
("PushFloat", format!("{}", val))
}
Op::PushString => {
let idx = self.reader.read_u32();
let s = self.ctx.resolve_string(idx);
let len = s.len();
let mut s_fmt = format!("{:?}", s);
if s_fmt.len() > 60 {
s_fmt.truncate(57);
write!(s_fmt, "...\" (total {len} bytes)")
.expect("writing to String is infallible");
}
("PushString", format!("@{} {}", idx, s_fmt))
}
Op::PushNull => ("PushNull", String::new()),
Op::PushTrue => ("PushTrue", String::new()),
Op::PushFalse => ("PushFalse", String::new()),
Op::LoadLocal => {
let idx = self.reader.read_u32();
("LoadLocal", format!("[{}]", idx))
}
Op::LoadOuter => {
let depth = self.reader.read_u8();
let idx = self.reader.read_u32();
("LoadOuter", format!("depth={} [{}]", depth, idx))
}
Op::StoreLocal => {
let idx = self.reader.read_u32();
("StoreLocal", format!("[{}]", idx))
}
Op::AllocLocals => {
let count = self.reader.read_u32();
("AllocLocals", format!("count={}", count))
}
Op::MakeThunk => {
let offset = self.reader.read_u32();
("MakeThunk", format!("-> {:04x}", offset))
}
Op::MakeClosure => {
let offset = self.reader.read_u32();
let slots = self.reader.read_u32();
("MakeClosure", format!("-> {:04x} slots={}", offset, slots))
}
Op::MakePatternClosure => {
let offset = self.reader.read_u32();
let slots = self.reader.read_u32();
let req_count = self.reader.read_u16();
let opt_count = self.reader.read_u16();
let ellipsis = self.reader.read_u8() != 0;
let mut arg_str = format!(
"-> {:04x} slots={} req={} opt={} ...={})",
offset, slots, req_count, opt_count, ellipsis
);
arg_str.push_str(" Args=[");
for _ in 0..req_count {
let idx = self.reader.read_u32();
arg_str.push_str(&format!("Req({}) ", self.ctx.resolve_string(idx)));
}
for _ in 0..opt_count {
let idx = self.reader.read_u32();
arg_str.push_str(&format!("Opt({}) ", self.ctx.resolve_string(idx)));
}
let total_args = req_count + opt_count;
for _ in 0..total_args {
let _name_idx = self.reader.read_u32();
let _span_id = self.reader.read_u32();
}
arg_str.push(']');
("MakePatternClosure", arg_str)
}
Op::Call => {
let _ = self.reader.read_operand_data();
("Call", "arg=?".into())
}
Op::DispatchCont => {
let phase =
Continuation::try_from(self.reader.read_u8()).expect("invalid primop phase");
("DispatchPrimOp", format!("phase={phase:?}"))
}
Op::MakeAttrs => {
let static_count = self.reader.read_u32();
let dynamic_count = self.reader.read_u32();
let mut args = format!("static={} dynamic={}", static_count, dynamic_count);
for _ in 0..static_count {
let key_id = self.reader.read_u32();
let _ = write!(args, " [{}={}", self.ctx.resolve_string(key_id), key_id);
let _ = self.reader.read_operand_data();
let _span_id = self.reader.read_u32();
args.push(']');
}
for _ in 0..dynamic_count {
let _ = write!(args, " [dyn");
let _ = self.reader.read_operand_data();
let _span_id = self.reader.read_u32();
args.push(']');
}
("MakeAttrs", args)
}
Op::MakeEmptyAttrs => ("MakeEmptyAttrs", String::new()),
Op::SelectStatic => {
let span_id = self.reader.read_u32();
let key_id = self.reader.read_u32();
(
"SelectStatic",
format!("key={} span={}", self.ctx.resolve_string(key_id), span_id),
)
}
Op::SelectDynamic => {
let span_id = self.reader.read_u32();
("SelectDynamic", format!("span={}", span_id))
}
Op::HasAttrPathStatic => {
let span_id = self.reader.read_u32();
let key_id = self.reader.read_u32();
(
"HasAttrPathStatic",
format!("key={} span={}", self.ctx.resolve_string(key_id), span_id),
)
}
Op::HasAttrPathDynamic => {
let span_id = self.reader.read_u32();
("HasAttrPathDynamic", format!("span={}", span_id))
}
Op::HasAttrStatic => {
let key_id = self.reader.read_u32();
(
"HasAttrStatic",
format!("key={}", self.ctx.resolve_string(key_id)),
)
}
Op::HasAttrDynamic => ("HasAttrDynamic", String::new()),
Op::HasAttrResolve => ("HasAttrResolve", String::new()),
Op::JumpIfSelectSucceeded => {
let offset = self.reader.read_i32();
let target = (current_pc as isize + 1 + 4 + offset as isize) as usize;
(
"JumpIfSelectSucceeded",
format!("-> {:04x} offset={}", target, offset),
)
}
Op::JumpIfSelectFailed => {
let offset = self.reader.read_i32();
let target = (current_pc as isize + 1 + 4 + offset as isize) as usize;
(
"JumpIfSelectFailed",
format!("-> {:04x} offset={}", target, offset),
)
}
Op::MakeList => {
let count = self.reader.read_u32();
for _ in 0..count {
let _ = self.reader.read_operand_data();
}
("MakeList", format!("size={}", count))
}
Op::MakeEmptyList => ("MakeEmptyList", String::new()),
Op::OpAdd => ("OpAdd", String::new()),
Op::OpSub => ("OpSub", String::new()),
Op::OpMul => ("OpMul", String::new()),
Op::OpDiv => ("OpDiv", String::new()),
Op::OpEq => ("OpEq", String::new()),
Op::OpNeq => ("OpNeq", String::new()),
Op::OpLt => ("OpLt", String::new()),
Op::OpGt => ("OpGt", String::new()),
Op::OpLeq => ("OpLeq", String::new()),
Op::OpGeq => ("OpGeq", String::new()),
Op::OpConcat => ("OpConcat", String::new()),
Op::OpUpdate => ("OpUpdate", String::new()),
Op::OpNeg => ("OpNeg", String::new()),
Op::OpNot => ("OpNot", String::new()),
Op::JumpIfFalse => {
let offset = self.reader.read_i32();
let target = (current_pc as isize + 1 + 4 + offset as isize) as usize;
(
"JumpIfFalse",
format!("-> {:04x} offset={}", target, offset),
)
}
Op::JumpIfTrue => {
let offset = self.reader.read_i32();
let target = (current_pc as isize + 1 + 4 + offset as isize) as usize;
("JumpIfTrue", format!("-> {:04x} offset={}", target, offset))
}
Op::Jump => {
let offset = self.reader.read_i32();
let target = (current_pc as isize + 1 + 4 + offset as isize) as usize;
("Jump", format!("-> {:04x} offset={}", target, offset))
}
Op::ConcatStrings => {
let count = self.reader.read_u16();
let force = self.reader.read_u8();
("ConcatStrings", format!("count={} force={}", count, force))
}
Op::CoerceToString => ("CoerceToString", String::new()),
Op::ResolvePath => {
let dir_id = self.reader.read_u32();
let dir = self.ctx.resolve_string(dir_id);
("ResolvePath", format!("dir={:?}", dir))
}
Op::Assert => {
let raw_idx = self.reader.read_u32();
let span_id = self.reader.read_u32();
("Assert", format!("text_id={} span={}", raw_idx, span_id))
}
Op::LookupWith => {
let idx = self.reader.read_u32();
let name = self.ctx.resolve_string(idx);
let n = self.reader.read_u8();
for _ in 0..n {
let _ = self.reader.read_operand_data();
}
("LookupWith", format!("sym={:?} n={}", name, n))
}
Op::LoadBuiltins => ("LoadBuiltins", String::new()),
Op::LoadBuiltin => {
let id = self.reader.read_u8();
("LoadBuiltin", format!("id={}", id))
}
Op::LoadReplBinding => {
let idx = self.reader.read_u32();
let name = self.ctx.resolve_string(idx);
("LoadReplBinding", format!("{:?}", name))
}
Op::LoadScopedBinding => {
let slot = self.reader.read_u32();
let idx = self.reader.read_u32();
let name = self.ctx.resolve_string(idx);
("LoadScopedBinding", format!("slot={} {:?}", slot, name))
}
Op::Return => ("Return", String::new()),
Op::Illegal => ("Illegal", String::new()),
}
}
}
-614
View File
@@ -1,614 +0,0 @@
#![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
}
}
-22
View File
@@ -1,22 +0,0 @@
[package]
name = "fix-compiler"
version = "0.1.0"
edition = "2024"
[dependencies]
bumpalo = { workspace = true }
colored = "3.1.1"
ghost-cell = { workspace = true }
hashbrown = { workspace = true }
rnix = { workspace = true }
rowan = { workspace = true }
string-interner = { workspace = true }
fix-bytecode = { path = "../fix-bytecode" }
fix-error = { path = "../fix-error" }
fix-lang = { path = "../fix-lang" }
fix-runtime = { path = "../fix-runtime" }
tracing = "0.1"
[lints]
workspace = true
-570
View File
@@ -1,570 +0,0 @@
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;
-327
View File
@@ -1,327 +0,0 @@
use std::hash::Hash;
use std::marker::PhantomData;
use bumpalo::Bump;
use bumpalo::collections::Vec;
use fix_lang::{BuiltinId, StringId};
use ghost_cell::{GhostCell, GhostToken};
use rnix::{TextRange, ast};
use string_interner::DefaultStringInterner;
pub mod downgrade;
pub type HashMap<'ir, K, V> = hashbrown::HashMap<K, V, hashbrown::DefaultHashBuilder, &'ir Bump>;
pub type GhostIrRef<'id, 'ir> = <GhostRef<'id, 'ir> as RefExt<'ir>>::IrRef;
pub type GhostRoIrRef<'id, 'ir> = <GhostRoRef<'id, 'ir> as RefExt<'ir>>::IrRef;
pub type RawIrRef<'ir> = <RawRef<'ir> as RefExt<'ir>>::IrRef;
pub type GhostMaybeThunkRef<'id, 'ir> = <GhostRef<'id, 'ir> as RefExt<'ir>>::MaybeThunkRef;
pub type GhostRoMaybeThunkRef<'id, 'ir> = <GhostRoRef<'id, 'ir> as RefExt<'ir>>::MaybeThunkRef;
impl<'id, 'ir> Ir<'ir, GhostRoRef<'id, 'ir>> {
/// Freeze a mutable IR reference into a read-only one, consuming the
/// `GhostToken` to prevent any further mutation.
pub fn freeze(this: GhostRoIrRef<'id, 'ir>, _: GhostToken<'id>) -> RawIrRef<'ir> {
// SAFETY: The transmute is sound because:
// - `GhostCell<'id, T>` is `#[repr(transparent)]` over `T`, so
// `&'ir GhostCell<'id, T>` and `&'ir T` have identical layout.
// - `Ir<'ir, R>` is `#[repr(C)]`, and for every field that depends on
// `R`, instantiating `R = GhostRef<'id, 'ir>` vs `R = RawRef<'ir>`
// produces types of identical layout:
// - `R::IrRef` becomes `&'ir GhostCell<'id, Ir<...>>` vs `&'ir Ir<...>`
// - `R::MaybeThunkRef` becomes `&'ir GhostCell<'id, MaybeThunk>`
// vs `&'ir MaybeThunk`
// - `R::Ref<Ir<'ir, R>>` (used in `ConcatStrings::parts`) reduces
// to the same case as `R::IrRef`
// - Therefore `IrRef<'id, 'ir>` and `RawIrRef<'ir>` are both
// pointer-sized references with the same layout.
//
// Consuming the `GhostToken` guarantees no `borrow_mut` calls can
// occur afterwards, so the shared `&Ir` references reachable from a
// `RawIrRef<'ir>` can never alias with mutable references.
unsafe { std::mem::transmute::<GhostRoIrRef<'id, 'ir>, RawIrRef<'ir>>(this) }
}
}
#[repr(transparent)]
pub struct GhostRoCell<'id, T: ?Sized>(GhostCell<'id, T>);
impl<'id, T> From<GhostCell<'id, T>> for GhostRoCell<'id, T> {
fn from(value: GhostCell<'id, T>) -> Self {
Self(value)
}
}
impl<'id, T: ?Sized> From<&GhostCell<'id, T>> for &GhostRoCell<'id, T> {
fn from(value: &GhostCell<'id, T>) -> Self {
// SAFETY: `GhostRoCell` is `#[repr(transparent)]` over `GhostCell`
// TODO: document mutability
unsafe { std::mem::transmute(value) }
}
}
impl<'id, T: ?Sized> From<&T> for &GhostRoCell<'id, T> {
fn from(value: &T) -> Self {
// SAFETY: `GhostRoCell` is `#[repr(transparent)]` over `GhostCell`,
// which is `#[repr(transparent)]` over `T`
// TODO: document mutability
unsafe { std::mem::transmute(value) }
}
}
impl<'id, T: ?Sized> GhostRoCell<'id, T> {
pub fn borrow<'a>(&'a self, token: &'a GhostToken<'id>) -> &'a T {
self.0.borrow(token)
}
}
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub enum MaybeThunk {
Int(i64),
Float(f64),
Bool(bool),
Null,
Str(StringId),
Path(StringId),
Thunk(ThunkId),
Arg { layer: u8 },
Builtin(BuiltinId),
BuiltinConst(StringId),
Builtins,
ReplBinding(StringId),
ScopedImportBinding { slot_id: u32, sym: StringId },
}
pub trait Ref<'ir> {
type Ref<T>
where
T: 'ir;
}
pub trait RefExt<'ir>: Ref<'ir> {
type Ir;
type IrRef;
type MaybeThunkRef;
}
impl<'ir, T: Ref<'ir> + 'ir> RefExt<'ir> for T {
type Ir = Ir<'ir, Self>;
type IrRef = Self::Ref<Self::Ir>;
type MaybeThunkRef = Self::Ref<MaybeThunk>;
}
pub struct GhostRef<'id, 'ir>(PhantomData<&'ir GhostCell<'id, ()>>);
pub struct GhostRoRef<'id, 'ir>(PhantomData<&'ir GhostRoCell<'id, ()>>);
pub struct RawRef<'ir>(PhantomData<&'ir ()>);
impl<'id, 'ir> Ref<'ir> for GhostRef<'id, 'ir> {
type Ref<T: 'ir> = &'ir GhostCell<'id, T>;
}
impl<'id, 'ir> Ref<'ir> for GhostRoRef<'id, 'ir> {
type Ref<T: 'ir> = &'ir GhostRoCell<'id, T>;
}
impl<'ir> Ref<'ir> for RawRef<'ir> {
type Ref<T: 'ir> = &'ir T;
}
#[repr(C)]
#[derive(Debug)]
pub enum Ir<'ir, R: RefExt<'ir> + ?Sized + 'ir> {
Int(i64),
Float(f64),
Bool(bool),
Null,
Str(StringId),
Path(R::IrRef),
AttrSet {
stcs: HashMap<'ir, StringId, (R::MaybeThunkRef, TextRange)>,
dyns: Vec<'ir, (R::IrRef, R::MaybeThunkRef, TextRange)>,
},
List {
items: Vec<'ir, R::MaybeThunkRef>,
},
ConcatStrings {
parts: Vec<'ir, R::Ref<Ir<'ir, R>>>,
force_string: bool,
},
// OPs
UnOp {
rhs: R::IrRef,
kind: UnOpKind,
},
BinOp {
lhs: R::IrRef,
rhs: R::IrRef,
kind: BinOpKind,
},
HasAttr {
lhs: R::IrRef,
rhs: Vec<'ir, Attr<R::IrRef>>,
},
Select {
expr: R::IrRef,
attrpath: Vec<'ir, Attr<R::IrRef>>,
default: Option<R::IrRef>,
span: TextRange,
},
// Conditionals
If {
cond: R::IrRef,
consq: R::IrRef,
alter: R::IrRef,
},
Assert {
assertion: R::IrRef,
expr: R::IrRef,
assertion_raw: String,
span: TextRange,
},
WithLookup {
sym: StringId,
namespaces: Vec<'ir, R::MaybeThunkRef>,
},
// Function related
Func {
body: R::IrRef,
param: Option<Param<'ir>>,
thunks: Vec<'ir, (ThunkId, R::IrRef)>,
},
Arg {
layer: u8,
},
Call {
func: R::IrRef,
arg: R::MaybeThunkRef,
span: TextRange,
},
// Builtins
Builtins,
Builtin(BuiltinId),
BuiltinConst(StringId),
// Misc
TopLevel {
body: R::IrRef,
thunks: Vec<'ir, (ThunkId, R::IrRef)>,
},
MaybeThunk(R::MaybeThunkRef),
ReplBinding(StringId),
ScopedImportBinding {
sym: StringId,
slot_id: u32,
},
}
#[repr(transparent)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ThunkId(pub usize);
/// Represents a key in an attribute path.
#[derive(Debug)]
pub enum Attr<Ref> {
/// A dynamic attribute key, which is an expression that must evaluate to a string.
/// Example: `attrs.${key}`
Dynamic(Ref, TextRange),
/// A static attribute key.
/// Example: `attrs.key`
Str(StringId, TextRange),
}
/// The kinds of binary operations supported in Nix.
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
pub enum BinOpKind {
// Arithmetic
Add,
Sub,
Div,
Mul,
// Comparison
Eq,
Neq,
Lt,
Gt,
Leq,
Geq,
// Logical
And,
Or,
Impl,
// Set/String/Path operations
Con, // List concatenation (`++`)
Upd, // AttrSet update (`//`)
}
/// The kinds of unary operations.
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
pub enum UnOpKind {
Neg, // Negation (`-`)
Not, // Logical not (`!`)
}
impl From<ast::UnaryOpKind> for UnOpKind {
fn from(value: ast::UnaryOpKind) -> Self {
match value {
ast::UnaryOpKind::Invert => UnOpKind::Not,
ast::UnaryOpKind::Negate => UnOpKind::Neg,
}
}
}
/// Describes the parameters of a function.
#[derive(Debug)]
pub struct Param<'ir> {
pub required: Vec<'ir, (StringId, TextRange)>,
pub optional: Vec<'ir, (StringId, TextRange)>,
pub ellipsis: bool,
}
pub fn new_global_env(
strings: &mut DefaultStringInterner,
) -> hashbrown::HashMap<StringId, MaybeThunk> {
let mut global_env = hashbrown::HashMap::new();
let builtins_sym = StringId(strings.get_or_intern("builtins"));
global_env.insert(builtins_sym, MaybeThunk::Builtins);
for id in BuiltinId::ALL {
let name = StringId(strings.get_or_intern(id.info().global_name));
global_env.insert(name, MaybeThunk::Builtin(id));
}
let consts = [
(
"__currentSystem",
MaybeThunk::BuiltinConst(StringId(strings.get_or_intern("currentSystem"))),
),
("__langVersion", MaybeThunk::Int(6)),
(
"__nixVersion",
MaybeThunk::BuiltinConst(StringId(strings.get_or_intern("nixVersion"))),
),
(
"__storeDir",
MaybeThunk::BuiltinConst(StringId(strings.get_or_intern("storeDir"))),
),
(
"__nixPath",
MaybeThunk::BuiltinConst(StringId(strings.get_or_intern("nixPath"))),
),
("null", MaybeThunk::Null),
("true", MaybeThunk::Bool(true)),
("false", MaybeThunk::Bool(false)),
];
for (name, ir) in consts {
let name = StringId(strings.get_or_intern(name));
global_env.insert(name, ir);
}
global_env
}
-12
View File
@@ -1,12 +0,0 @@
[package]
name = "fix-error"
version = "0.1.0"
edition = "2024"
[dependencies]
miette = { version = "7.6", features = ["fancy"] }
rnix = { workspace = true }
thiserror = "2.0"
[lints]
workspace = true
-223
View File
@@ -1,223 +0,0 @@
use std::path::{Path, PathBuf};
use std::sync::Arc;
use miette::{Diagnostic, NamedSource, SourceSpan};
use thiserror::Error;
pub type Result<T, E = Box<Error>> = core::result::Result<T, E>;
#[derive(Clone, Debug)]
pub enum SourceType {
/// dir
Eval(Arc<PathBuf>),
/// dir
Repl(Arc<PathBuf>),
/// file
File(Arc<PathBuf>),
/// virtual (name, no path)
Virtual(Arc<str>),
}
#[derive(Clone, Debug)]
pub struct Source {
pub ty: SourceType,
pub src: Arc<str>,
}
impl TryFrom<&str> for Source {
type Error = Box<Error>;
fn try_from(value: &str) -> Result<Self> {
Source::new_eval(value.into())
}
}
impl From<Source> for NamedSource<Arc<str>> {
fn from(value: Source) -> Self {
let name = value.get_name();
NamedSource::new(name, value.src.clone())
}
}
impl Source {
pub fn new_file(path: PathBuf) -> std::io::Result<Self> {
Ok(Source {
src: std::fs::read_to_string(&path)?.into(),
ty: SourceType::File(Arc::new(path)),
})
}
pub fn new_eval(src: String) -> Result<Self> {
Ok(Self {
ty: std::env::current_dir()
.map_err(|err| Error::internal(format!("Failed to get current working dir: {err}")))
.map(Arc::new)
.map(SourceType::Eval)?,
src: src.into(),
})
}
pub fn new_repl(src: String) -> Result<Self> {
Ok(Self {
ty: std::env::current_dir()
.map_err(|err| Error::internal(format!("Failed to get current working dir: {err}")))
.map(Arc::new)
.map(SourceType::Repl)?,
src: src.into(),
})
}
pub fn new_virtual(name: Arc<str>, src: String) -> Self {
Self {
ty: SourceType::Virtual(name),
src: src.into(),
}
}
pub fn get_dir(&self) -> &Path {
use SourceType::*;
match &self.ty {
Eval(dir) | Repl(dir) => dir.as_ref(),
File(file) => file
.as_path()
.parent()
.expect("source file must have a parent dir"),
Virtual(_) => Path::new("/"),
}
}
pub fn get_name(&self) -> String {
match &self.ty {
SourceType::Eval(_) => "«eval»".into(),
SourceType::Repl(_) => "«repl»".into(),
SourceType::File(path) => path.as_os_str().to_string_lossy().to_string(),
SourceType::Virtual(name) => name.to_string(),
}
}
}
#[derive(Error, Debug, Diagnostic)]
pub enum Error {
#[error("Parse error: {message}")]
#[diagnostic(code(nix::parse))]
ParseError {
#[source_code]
src: Option<NamedSource<Arc<str>>>,
#[label("error occurred here")]
span: Option<SourceSpan>,
message: String,
},
#[error("Downgrade error: {message}")]
#[diagnostic(code(nix::downgrade))]
DowngradeError {
#[source_code]
src: Option<NamedSource<Arc<str>>>,
#[label("{message}")]
span: Option<SourceSpan>,
message: String,
},
#[error("Evaluation error: {message}")]
#[diagnostic(code(nix::eval))]
EvalError {
#[source_code]
src: Option<NamedSource<Arc<str>>>,
#[label("error occurred here")]
span: Option<SourceSpan>,
message: String,
#[related]
stack_trace: Vec<StackFrame>,
},
#[error("Internal error: {message}")]
#[diagnostic(code(nix::internal))]
InternalError { message: String },
#[error("{message}")]
#[diagnostic(code(nix::catchable))]
Catchable { message: String },
#[error("Unknown error")]
#[diagnostic(code(nix::unknown))]
Unknown,
}
impl Error {
pub fn parse_error(msg: String) -> Box<Self> {
Error::ParseError {
src: None,
span: None,
message: msg,
}
.into()
}
pub fn downgrade_error(msg: String, src: Source, span: rnix::TextRange) -> Box<Self> {
Error::DowngradeError {
src: Some(src.into()),
span: Some(text_range_to_source_span(span)),
message: msg,
}
.into()
}
pub fn eval_error(msg: impl Into<String>) -> Box<Self> {
Error::EvalError {
src: None,
span: None,
message: msg.into(),
stack_trace: Vec::new(),
}
.into()
}
pub fn internal(msg: String) -> Box<Self> {
Error::InternalError { message: msg }.into()
}
pub fn catchable(msg: String) -> Box<Self> {
Error::Catchable { message: msg }.into()
}
pub fn with_span(mut self: Box<Self>, span: rnix::TextRange) -> Box<Self> {
use Error::*;
let source_span = Some(text_range_to_source_span(span));
let (ParseError { span, .. } | DowngradeError { span, .. } | EvalError { span, .. }) =
self.as_mut()
else {
return self;
};
*span = source_span;
self
}
pub fn with_source(mut self: Box<Self>, source: Source) -> Box<Self> {
use Error::*;
let new_src = Some(source.into());
let (ParseError { src, .. } | DowngradeError { src, .. } | EvalError { src, .. }) =
self.as_mut()
else {
return self;
};
*src = new_src;
self
}
}
pub fn text_range_to_source_span(range: rnix::TextRange) -> SourceSpan {
let start = usize::from(range.start());
let len = usize::from(range.end()) - start;
SourceSpan::new(start.into(), len)
}
/// Stack frame types from Nix evaluation
#[derive(Debug, Clone, Error, Diagnostic)]
#[error("{message}")]
pub struct StackFrame {
#[label]
pub span: SourceSpan,
#[help]
pub message: String,
#[source_code]
pub src: NamedSource<Arc<str>>,
}
-13
View File
@@ -1,13 +0,0 @@
[package]
name = "fix-lang"
version = "0.1.0"
edition = "2024"
[dependencies]
ere = { workspace = true }
gc-arena = { workspace = true }
num_enum = { workspace = true }
string-interner = { workspace = true }
[lints]
workspace = true
-15
View File
@@ -1,15 +0,0 @@
[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
@@ -1,49 +0,0 @@
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()
}
-19
View File
@@ -1,19 +0,0 @@
[package]
name = "fix-runtime"
version = "0.1.0"
edition = "2024"
[dependencies]
gc-arena = { workspace = true }
hashbrown = { workspace = true }
smallvec = { workspace = true }
sptr = "0.3"
string-interner = { workspace = true }
fix-bytecode = { path = "../fix-bytecode" }
fix-error = { path = "../fix-error" }
fix-lang = { path = "../fix-lang" }
fix-macros = { path = "../fix-macros" }
[lints]
workspace = true
-235
View File
@@ -1,235 +0,0 @@
use fix_lang::StringId;
use gc_arena::Mutation;
use crate::{
AttrSet, BytecodeReader, Closure, List, Machine, NixNum, NixString, NixType, Null, PrimOp,
PrimOpApp, Step, StrictValue, ValueVariant,
};
pub trait Forced<'gc>: Sized {
const WIDTH: usize;
/// Force and type-check the `WIDTH` slots starting at `base_depth` from
/// TOS, deepest-first. If a slot holds a thunk, enter it and return
/// `Break::Force`. If a slot holds a value of the wrong type, call
/// `finish_type_err` and return `Break::Done`.
fn force_and_check<M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
base_depth: usize,
resume_pc: usize,
) -> Step;
/// After `force_and_check` returned `Continue`, pop `WIDTH` slots
/// (TOS first) and convert. Type assertions are infallible because
/// `force_and_check` already validated every slot.
fn pop_converted<M: Machine<'gc>>(m: &mut M) -> Self;
}
impl<'gc> Forced<'gc> for StrictValue<'gc> {
const WIDTH: usize = 1;
#[inline(always)]
fn force_and_check<M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
base_depth: usize,
resume_pc: usize,
) -> Step {
m.force_slot_to_pc(base_depth, reader, mc, resume_pc)
}
#[inline(always)]
fn pop_converted<M: Machine<'gc>>(m: &mut M) -> Self {
m.pop_forced()
}
}
macro_rules! impl_forced {
($($ty:ty),* $(,)?) => {
$(
impl<'gc> Forced<'gc> for <$ty as ValueVariant<'gc>>::Ty {
const WIDTH: usize = 1;
#[inline(always)]
fn force_and_check<M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
base_depth: usize,
resume_pc: usize,
) -> Step {
m.force_slot_to_pc(base_depth, reader, mc, resume_pc)?;
let v = m.peek_forced(base_depth);
if !v.is::<$ty>() {
m.finish_type_err(<$ty as ValueVariant>::TYPE, v.ty())
} else {
Step::Continue(())
}
}
#[inline(always)]
fn pop_converted<M: Machine<'gc>>(m: &mut M) -> Self {
m.pop_forced()
.downcast::<$ty>()
.expect("type checked in force_and_check")
}
}
)*
};
}
impl_forced! {
i32,
bool,
Null,
StringId,
PrimOp,
i64,
NixString,
AttrSet<'gc>,
List<'gc>,
Closure<'gc>,
PrimOpApp<'gc>,
}
impl<'gc> Forced<'gc> for NixNum {
const WIDTH: usize = 1;
#[inline(always)]
fn force_and_check<M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
base_depth: usize,
resume_pc: usize,
) -> Step {
m.force_slot_to_pc(base_depth, reader, mc, resume_pc)?;
let v = m.peek_forced(base_depth);
if v.downcast_num().is_none() {
m.finish_type_err(NixType::Int, v.ty())
} else {
Step::Continue(())
}
}
#[inline(always)]
fn pop_converted<M: Machine<'gc>>(m: &mut M) -> Self {
m.pop_forced()
.downcast_num()
.expect("type checked in force_and_check")
}
}
impl<'gc> Forced<'gc> for f64 {
const WIDTH: usize = 1;
#[inline(always)]
fn force_and_check<M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
base_depth: usize,
resume_pc: usize,
) -> Step {
m.force_slot_to_pc(base_depth, reader, mc, resume_pc)?;
let v = m.peek_forced(base_depth);
if !v.is::<f64>() {
m.finish_type_err(NixType::Float, v.ty())
} else {
Step::Continue(())
}
}
#[inline(always)]
fn pop_converted<M: Machine<'gc>>(m: &mut M) -> Self {
m.pop_forced()
.downcast::<f64>()
.expect("type checked in force_and_check")
}
}
impl<'gc, A: Forced<'gc>, B: Forced<'gc>> Forced<'gc> for (A, B) {
const WIDTH: usize = A::WIDTH + B::WIDTH;
#[inline(always)]
fn force_and_check<M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
base: usize,
resume_pc: usize,
) -> Step {
A::force_and_check(m, reader, mc, base + B::WIDTH, resume_pc)?;
B::force_and_check(m, reader, mc, base, resume_pc)
}
#[inline(always)]
fn pop_converted<M: Machine<'gc>>(m: &mut M) -> Self {
let b = B::pop_converted(m);
let a = A::pop_converted(m);
(a, b)
}
}
impl<'gc, A: Forced<'gc>, B: Forced<'gc>, C: Forced<'gc>> Forced<'gc> for (A, B, C) {
const WIDTH: usize = A::WIDTH + B::WIDTH + C::WIDTH;
#[inline(always)]
fn force_and_check<M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
base: usize,
resume_pc: usize,
) -> Step {
A::force_and_check(m, reader, mc, base + B::WIDTH + C::WIDTH, resume_pc)?;
B::force_and_check(m, reader, mc, base + C::WIDTH, resume_pc)?;
C::force_and_check(m, reader, mc, base, resume_pc)
}
#[inline(always)]
fn pop_converted<M: Machine<'gc>>(m: &mut M) -> Self {
let c = C::pop_converted(m);
let b = B::pop_converted(m);
let a = A::pop_converted(m);
(a, b, c)
}
}
impl<'gc, A: Forced<'gc>, B: Forced<'gc>, C: Forced<'gc>, D: Forced<'gc>> Forced<'gc>
for (A, B, C, D)
{
const WIDTH: usize = A::WIDTH + B::WIDTH + C::WIDTH + D::WIDTH;
#[inline(always)]
fn force_and_check<M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
base: usize,
resume_pc: usize,
) -> Step {
A::force_and_check(
m,
reader,
mc,
base + B::WIDTH + C::WIDTH + D::WIDTH,
resume_pc,
)?;
B::force_and_check(m, reader, mc, base + C::WIDTH + D::WIDTH, resume_pc)?;
C::force_and_check(m, reader, mc, base + D::WIDTH, resume_pc)?;
D::force_and_check(m, reader, mc, base, resume_pc)
}
#[inline(always)]
fn pop_converted<M: Machine<'gc>>(m: &mut M) -> Self {
let d = D::pop_converted(m);
let c = C::pop_converted(m);
let b = B::pop_converted(m);
let a = A::pop_converted(m);
(a, b, c, d)
}
}
-163
View File
@@ -1,163 +0,0 @@
use fix_bytecode::InstructionPtr;
use fix_error::Source;
use fix_lang::{self, StringId};
use hashbrown::HashSet;
use crate::{
AttrSet, Closure, ExtraScope, List, NixString, NixType, Null, Path, PrimOp, PrimOpApp,
StaticValue, StrictValue, StringContext, Thunk, ThunkState, Value,
};
pub trait VmContext {
fn split(&mut self) -> (&mut impl VmCode, &mut impl VmRuntimeCtx);
}
pub trait VmRuntimeCtx {
fn intern_string(&mut self, s: impl AsRef<str>) -> StringId;
fn resolve_string(&self, id: StringId) -> &str;
fn get_const(&self, id: u32) -> StaticValue;
fn add_const(&mut self, val: StaticValue) -> u32;
}
pub trait VmCode {
fn bytecode(&self) -> &[u8];
fn compile_with_scope(
&mut self,
source: Source,
extra_scope: Option<ExtraScope>,
ctx: &mut impl VmRuntimeCtx,
) -> fix_error::Result<InstructionPtr>;
}
pub trait VmRuntimeCtxExt: VmRuntimeCtx {
fn get_string<'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>;
fn get_string_id<'a, 'gc: 'a>(
&'a mut self,
val: StrictValue<'gc>,
) -> std::result::Result<StringId, NixType>;
/// Returns the string context attached to `val`, or `&[]` if `val` is
/// either a non-string or a string without context.
fn get_string_context<'gc>(&self, val: StrictValue<'gc>) -> &'gc StringContext;
fn convert_value(&self, val: Value) -> fix_lang::Value;
}
impl<T: VmRuntimeCtx> VmRuntimeCtxExt for T {
fn get_string<'a, 'gc: 'a>(&'a self, val: StrictValue<'gc>) -> Option<&'a str> {
if let Some(sid) = val.downcast::<StringId>() {
Some(self.resolve_string(sid))
} else {
val.downcast::<NixString>().map(|ns| ns.as_ref().as_str())
}
}
/// Like `get_string`, but also accepts `Path` values (returning their
/// underlying canonical-path string). Use this in places where Nix
/// would coerce a path to a string (string interpolation, file IO
/// builtins, etc.).
fn get_string_or_path<'a, 'gc: 'a>(&'a self, val: StrictValue<'gc>) -> Option<&'a str> {
if let Some(p) = val.downcast::<Path>() {
Some(self.resolve_string(p.0))
} else {
self.get_string(val)
}
}
fn get_string_id<'a, 'gc: 'a>(
&'a mut self,
val: StrictValue<'gc>,
) -> std::result::Result<StringId, NixType> {
if let Some(sid) = val.downcast::<StringId>() {
Ok(sid)
} else if let Some(s) = val.downcast::<NixString>().map(|ns| ns.as_ref().as_str()) {
Ok(self.intern_string(s))
} else {
Err(val.ty())
}
}
fn get_string_context<'gc>(&self, val: StrictValue<'gc>) -> &'gc StringContext {
if let Some(ns) = val.downcast::<NixString>() {
ns.as_ref().context()
} else {
StringContext::empty()
}
}
fn convert_value(&self, val: Value) -> fix_lang::Value {
self.convert_value_with_seen(val, &mut HashSet::new())
}
}
pub(crate) trait ConvertValueWithSeen: VmRuntimeCtx {
fn convert_value_with_seen(&self, val: Value, seen: &mut HashSet<u64>) -> fix_lang::Value;
}
impl<T: VmRuntimeCtx> ConvertValueWithSeen for T {
fn convert_value_with_seen(&self, val: Value, seen: &mut HashSet<u64>) -> fix_lang::Value {
use fix_lang::Value;
if let Some(i) = val.downcast::<i32>() {
Value::Int(i as i64)
} else if let Some(gc_i) = val.downcast::<i64>() {
Value::Int(*gc_i)
} else if let Some(f) = val.downcast::<f64>() {
Value::Float(f)
} else if let Some(b) = val.downcast::<bool>() {
Value::Bool(b)
} else if val.is::<Null>() {
Value::Null
} else if let Some(sid) = val.downcast::<StringId>() {
let s = self.resolve_string(sid).to_owned();
Value::String(s)
} else if let Some(ns) = val.downcast::<NixString>() {
Value::String(ns.as_str().to_owned())
} else if let Some(p) = val.downcast::<Path>() {
Value::Path(self.resolve_string(p.0).to_owned())
} else if let Some(attrs) = val.downcast::<AttrSet>() {
let bits = val.to_bits();
if attrs.entries.is_empty() {
return Value::AttrSet(Default::default());
}
if !seen.insert(bits) {
return Value::Repeated;
}
let mut map = std::collections::BTreeMap::new();
for &(key, val) in attrs.entries.iter() {
let key = self.resolve_string(key).to_owned();
let converted = self.convert_value_with_seen(val, seen);
map.insert(fix_lang::Symbol::from(key), converted);
}
Value::AttrSet(fix_lang::AttrSet::new(map))
} else if let Some(list) = val.downcast::<List>() {
let bits = val.to_bits();
if list.inner.borrow().is_empty() {
return Value::List(Default::default());
}
if !seen.insert(bits) {
return Value::Repeated;
}
let items: Vec<_> = list
.inner
.borrow()
.iter()
.copied()
.map(|v| self.convert_value_with_seen(v, seen))
.collect();
Value::List(fix_lang::List::new(items))
} else if val.is::<Closure>() {
Value::Func
} else if let Some(thunk) = val.downcast::<Thunk>() {
if let ThunkState::Evaluated(v) = *thunk.borrow() {
self.convert_value_with_seen(v.relax(), seen)
} else {
Value::Thunk
}
} else if let Some(primop) = val.downcast::<PrimOp>() {
Value::PrimOp(primop.id.info().name)
} else if let Some(app) = val.downcast::<PrimOpApp>() {
Value::PrimOpApp(app.primop.id.info().name)
} else {
Value::Null
}
}
}
-23
View File
@@ -1,23 +0,0 @@
mod boxing;
mod forced;
mod host;
mod machine;
mod macro_support;
mod path_util;
mod resolve;
mod slot;
mod state;
mod string_context;
mod value;
pub use fix_bytecode::{BytecodeReader, OperandData};
pub use forced::*;
pub use host::*;
pub use machine::*;
pub use macro_support::*;
pub use path_util::*;
pub use resolve::*;
pub use slot::*;
pub use state::*;
pub use string_context::*;
pub use value::*;
-187
View File
@@ -1,187 +0,0 @@
use std::ops::ControlFlow;
use std::path::{Path, PathBuf};
use fix_error::Error;
use fix_lang::{self, StringId};
use gc_arena::{Gc, Mutation};
use crate::{
AttrSet, Break, BytecodeReader, CallFrame, ForceMode, Forced, GcEnv, NixType, PendingLoad,
Step, StrictValue, Value, VmError,
};
/// Abstract VM-side operations consumed by instruction handlers and primops.
///
/// Implementors maintain a value stack, a call stack, an environment chain,
/// pending result/error state, and a set of GC-allocated globals. Methods
/// fall into a few groups:
///
/// - Stack ops (`push` / `pop` / `peek` / `replace` / `pop_forced` / ...)
/// - Forcing primitives (`force_slot` / `force_slot_to_pc`)
/// - Calling (`call` / `return_from_primop`)
/// - Call-frame management (`push_call_frame` / `pop_call_frame` / call-depth)
/// - Environment access (`env` / `set_env` / `local`)
/// - Result finalization (`finish_ok` / `finish_err` / ...)
/// - Global lookup (`builtins` / `empty_list` / `empty_attrs` / ...)
/// - Imports and scope slots (`import_cache_*` / `scope_slot*` / `set_pending_load`)
pub trait Machine<'gc> {
fn push(&mut self, val: Value<'gc>);
#[must_use]
fn pop(&mut self) -> Value<'gc>;
#[must_use]
fn peek(&self, depth: usize) -> Value<'gc>;
#[must_use]
fn peek_forced(&self, depth: usize) -> StrictValue<'gc>;
fn pop_forced(&mut self) -> StrictValue<'gc>;
fn replace(&mut self, depth: usize, val: Value<'gc>);
fn drop_n(&mut self, depth: usize);
fn stack_len(&self) -> usize;
fn force_slot_to_pc(
&mut self,
depth: usize,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
resume_pc: usize,
) -> Step;
#[inline(always)]
fn force_slot(
&mut self,
depth: usize,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let pc = reader.inst_start_pc();
self.force_slot_to_pc(depth, reader, mc, pc)
}
fn call(
&mut self,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
arg: Value<'gc>,
resume_pc: usize,
) -> Step;
#[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 {
self.push(val);
let Some(CallFrame {
pc: ret_pc,
thunk: _,
env,
depth: None,
}) = self.pop_call_frame()
else {
unreachable!()
};
reader.set_pc(ret_pc);
self.dec_call_depth();
self.set_env(env);
Step::Continue(())
}
fn push_call_frame(&mut self, frame: CallFrame<'gc>);
fn pop_call_frame(&mut self) -> Option<CallFrame<'gc>>;
fn call_depth(&self) -> usize;
fn inc_call_depth(&mut self);
fn dec_call_depth(&mut self);
fn env(&self) -> GcEnv<'gc>;
fn set_env(&mut self, env: GcEnv<'gc>);
#[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> {
let mut cur = self.env();
for _ in 0..layer {
let prev = cur.borrow().prev.expect("env chain too short");
cur = prev;
}
cur.borrow().locals[idx as usize]
}
fn finish_ok(&mut self, val: fix_lang::Value) -> Step;
fn finish_err(&mut self, err: Box<Error>) -> Step;
fn finish_type_err(&mut self, expected: NixType, got: NixType) -> Step;
#[inline(always)]
fn finish_vm_err(&mut self, err: VmError) -> Step {
self.finish_err(err.into_error())
}
fn builtins(&self) -> Gc<'gc, AttrSet<'gc>>;
fn functor_sym(&self) -> StringId;
fn empty_list(&self) -> Value<'gc>;
fn empty_attrs(&self) -> Value<'gc>;
fn force_mode(&self) -> ForceMode;
fn import_cache_get(&self, path: &Path) -> Option<Value<'gc>>;
fn import_cache_insert(&mut self, path: PathBuf, val: Value<'gc>);
fn scope_slot(&self, idx: u32) -> Value<'gc>;
fn scope_slots_push(&mut self, val: Value<'gc>) -> u32;
fn set_pending_load(&mut self, load: PendingLoad);
}
/// Extension trait with convenience helpers built on top of [`Machine`].
///
/// Auto-implemented for every `Machine<'gc>` so callers just need to bring
/// `MachineExt` (or `Machine`) into scope.
pub trait MachineExt<'gc>: Machine<'gc> {
/// 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.
/// * The stack layout at the call site must be **identical** every time
/// the handler is re-entered.
/// * Propagate the return value with `?` so `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>,
) -> ControlFlow<Break, T>
where
Self: Sized,
{
let pc = reader.inst_start_pc();
self.force_and_retry_pc(reader, mc, 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,
) -> ControlFlow<Break, T>
where
Self: Sized,
{
T::force_and_check(self, reader, mc, 0, resume_pc)?;
ControlFlow::Continue(T::pop_converted(self))
}
}
impl<'gc, M: Machine<'gc>> MachineExt<'gc> for M {}
-165
View File
@@ -1,165 +0,0 @@
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(())
}
}
-18
View File
@@ -1,18 +0,0 @@
use std::path::{Component, PathBuf};
pub fn canon_path_str(path: impl AsRef<std::path::Path>) -> String {
let p = path.as_ref();
let mut normalized = PathBuf::new();
for component in p.components() {
match component {
Component::Prefix(p) => normalized.push(p.as_os_str()),
Component::RootDir => normalized.push("/"),
Component::CurDir => {}
Component::ParentDir => {
normalized.pop();
}
Component::Normal(c) => normalized.push(c),
}
}
normalized.to_string_lossy().into_owned()
}
-32
View File
@@ -1,32 +0,0 @@
use fix_bytecode::OperandData;
use gc_arena::{Gc, Mutation};
use crate::{AttrSet, Machine, Value, VmRuntimeCtx};
/// Resolve a decoded operand into a runtime [`Value`].
///
/// The operand decoder ([`crate::BytecodeReader::read_operand_data`])
/// produces a static enum; this function materializes it against the
/// running [`Machine`] (env chain, builtins, scope slots, ...).
#[inline]
pub fn resolve_operand<'gc, M: Machine<'gc>>(
op: &OperandData,
mc: &Mutation<'gc>,
ctx: &impl VmRuntimeCtx,
m: &M,
) -> Value<'gc> {
use OperandData::*;
match *op {
Const(id) => ctx.get_const(id).into(),
BigInt(val) => Value::new(Gc::new(mc, val)),
Local { layer, idx } => m.local(layer, idx),
BuiltinConst(id) => m.builtins().lookup(id).expect("builtin const must exist"),
Builtins => m.builtins().into(),
ReplBinding(_id) => todo!(),
ScopedImportBinding { slot_id, name } => {
let scope = m.scope_slot(slot_id);
let attrs = scope.downcast::<AttrSet>().expect("scope must be attrset");
attrs.lookup(name).expect("scoped binding not found")
}
}
}
-86
View File
@@ -1,86 +0,0 @@
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)
}
}
-88
View File
@@ -1,88 +0,0 @@
use std::ops::ControlFlow;
use std::path::PathBuf;
use fix_error::Error;
use fix_lang::StringId;
use gc_arena::{Collect, Gc};
use hashbrown::HashSet;
use crate::{GcEnv, Thunk};
pub enum VmError {
Catchable(String),
Uncatchable(Box<Error>),
}
impl From<Box<Error>> for VmError {
fn from(e: Box<Error>) -> Self {
VmError::Uncatchable(e)
}
}
impl VmError {
pub fn into_error(self) -> Box<Error> {
match self {
VmError::Catchable(_) => todo!("Check for tryEval catch frames"),
VmError::Uncatchable(e) => e,
}
}
}
pub fn vm_err(msg: impl Into<String>) -> VmError {
VmError::Uncatchable(Error::eval_error(msg.into()))
}
#[derive(Collect, Clone, Copy, Debug, PartialEq, Eq, Default)]
#[collect(require_static)]
pub enum ForceMode {
#[default]
AsIs,
Shallow,
Deep,
}
#[repr(u8)]
pub enum Break {
Force,
Done,
LoadFile,
}
pub type Step = ControlFlow<Break>;
pub struct ErrorFrame {
pub span_id: u32,
pub message: Option<String>,
}
#[derive(Collect, Debug)]
#[collect(no_drop)]
pub struct CallFrame<'gc> {
pub pc: usize,
pub env: GcEnv<'gc>,
pub thunk: Option<Gc<'gc, Thunk<'gc>>>,
pub depth: Option<usize>,
}
#[derive(Debug)]
pub struct PendingLoad {
pub path: PathBuf,
pub scope: Option<PendingScope>,
}
#[derive(Debug)]
pub struct PendingScope {
pub keys: HashSet<StringId>,
pub slot_id: u32,
}
/// Extra scope passed to a re-entrant compile from inside a running VM.
///
/// Currently only `ScopedImport` is produced (by the `scopedImport` builtin),
/// but the variant is kept open so REPL bindings could later land here too.
pub enum ExtraScope {
ScopedImport {
keys: HashSet<StringId>,
slot_id: u32,
},
}
-165
View File
@@ -1,165 +0,0 @@
use std::cmp::Ordering;
use smallvec::SmallVec;
/// A string context element
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum StringContextElem {
// Plain store path reference
Opaque {
path: Box<str>,
},
// All outputs of a derivation
// encoded `=<drvPath>`
DrvDeep {
drv_path: Box<str>,
},
// A specific output of a derivation
// encoded `!<output>!<drvPath>`
Built {
drv_path: Box<str>,
output: Box<str>,
},
}
impl StringContextElem {
/// Decode the CppNix wire form (`!out!/p`, `=/p`, `/p`). Falls back to
/// `Opaque` for malformed `!`-prefixed inputs (matching nix-js).
pub fn decode(encoded: &str) -> Self {
if let Some(drv_path) = encoded.strip_prefix('=') {
Self::DrvDeep {
drv_path: drv_path.into(),
}
} else if let Some(rest) = encoded.strip_prefix('!') {
if let Some((output, drv_path)) = rest.split_once('!') {
Self::Built {
output: output.into(),
drv_path: drv_path.into(),
}
} else {
Self::Opaque {
path: encoded.into(),
}
}
} else {
Self::Opaque {
path: encoded.into(),
}
}
}
pub fn encode(&self) -> String {
match self {
Self::Opaque { path } => path.to_string(),
Self::DrvDeep { drv_path } => format!("={drv_path}"),
Self::Built { drv_path, output } => format!("!{output}!{drv_path}"),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct StringContext {
data: SmallVec<[StringContextElem; 1]>,
}
impl IntoIterator for StringContext {
type Item = StringContextElem;
type IntoIter = <SmallVec<[StringContextElem; 1]> as IntoIterator>::IntoIter;
fn into_iter(self) -> Self::IntoIter {
self.data.into_iter()
}
}
impl<'a> IntoIterator for &'a StringContext {
type Item = &'a StringContextElem;
type IntoIter = <&'a SmallVec<[StringContextElem; 1]> as IntoIterator>::IntoIter;
fn into_iter(self) -> Self::IntoIter {
self.data.iter()
}
}
impl<'a> IntoIterator for &'a mut StringContext {
type Item = &'a mut StringContextElem;
type IntoIter = <&'a mut SmallVec<[StringContextElem; 1]> as IntoIterator>::IntoIter;
fn into_iter(self) -> Self::IntoIter {
self.data.iter_mut()
}
}
impl FromIterator<StringContextElem> for StringContext {
fn from_iter<T: IntoIterator<Item = StringContextElem>>(iter: T) -> Self {
Self {
data: iter.into_iter().collect(),
}
}
}
impl StringContext {
pub fn empty() -> &'static Self {
static EMPTY: StringContext = StringContext {
data: SmallVec::new_const(),
};
&EMPTY
}
pub fn new() -> Self {
Self::default()
}
pub fn is_empty(&self) -> bool {
self.data.is_empty()
}
pub fn insert(&mut self, elem: StringContextElem) {
match self.data.binary_search(&elem) {
Ok(_) => {}
Err(pos) => self.data.insert(pos, elem),
}
}
#[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 {
if self.data.is_empty() {
return other.clone();
}
if other.data.is_empty() {
return self.clone();
}
let a = &self.data;
let b = &other.data;
let mut out = SmallVec::with_capacity(a.len() + b.len());
let (mut i, mut j) = (0, 0);
while i < a.len() && j < b.len() {
match a[i].cmp(&b[j]) {
Ordering::Less => {
out.push(a[i].clone());
i += 1;
}
Ordering::Greater => {
out.push(b[j].clone());
j += 1;
}
Ordering::Equal => {
out.push(a[i].clone());
i += 1;
j += 1;
}
}
}
out.extend(a[i..].iter().cloned());
out.extend(b[j..].iter().cloned());
Self { data: out }
}
pub fn iter(&self) -> <&Self as IntoIterator>::IntoIter {
self.into_iter()
}
pub fn iter_mut(&mut self) -> <&mut Self as IntoIterator>::IntoIter {
self.into_iter()
}
}
-783
View File
@@ -1,783 +0,0 @@
use std::cell::RefCell;
use std::fmt;
use std::marker::PhantomData;
use std::mem::size_of;
use std::ops::Deref;
use fix_bytecode::Continuation;
use fix_lang::*;
use fix_macros::unelide_lifetimes;
use gc_arena::barrier::Unlock;
use gc_arena::collect::Trace;
use gc_arena::{Collect, Gc, GcRefLock, Mutation, RefLock};
use smallvec::SmallVec;
use string_interner::Symbol;
use string_interner::symbol::SymbolU32;
use crate::boxing::{RawBox, RawStore, RawTag, Value as RawValue};
use crate::string_context::StringContext;
mod private {
pub trait Cealed {}
}
pub trait ValueVariant<'gc>: private::Cealed {
#[expect(
private_bounds,
reason = "Storable is a sealed implementation detail of the value system"
)]
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;
}
macro_rules! define_value_types {
(
inline { $($itype:ty => $itag:path, $ity:path, $iname:literal;)* }
gc { $($gtype:ty => $gtag:path, $gty:path, $gname:literal;)* }
) => {
$(
impl Storable<'_> for $itype {
#[inline(always)]
fn is(value: &RawBox) -> bool {
value.tag() == Some($itag)
}
#[inline(always)]
fn to_raw_box(self) -> RawBox {
RawBox::from_value(RawValue::store($itag, self))
}
#[inline(always)]
unsafe fn from_raw_box(raw: RawBox) -> Self {
// SAFETY: the caller guarantees `raw` represents a valid
// `Self` of this inline type, so it holds a `Value` (making
// `value()` `Some`) whose payload decodes to this type.
unsafe { <Self as RawStore>::from_val(raw.value().unwrap_unchecked()) }
}
}
impl 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(())
}
}
)*
$(
impl<'gc> Storable<'gc> for Gc<'gc, unelide_lifetimes!('gc; $gtype)> {
#[inline(always)]
fn is(value: &RawBox) -> bool {
value.tag() == Some($gtag)
}
#[inline(always)]
fn to_raw_box(self) -> RawBox {
RawBox::from_value(RawValue::store($gtag, Gc::as_ptr(self)))
}
#[inline(always)]
unsafe fn from_raw_box(raw: RawBox) -> Self {
// SAFETY: the caller guarantees `raw` represents a valid
// `Self` of this GC type, so it holds a `Value` (making
// `value()` `Some`) whose payload is the pointer originally
// produced by `Gc::as_ptr` in `to_raw_box`.
unsafe { Gc::from_ptr(<*mut $gtype as RawStore>::from_val(raw.value().unwrap_unchecked())) }
}
}
impl<'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<'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::<$itype>() <= 6);)*
const _: () = {
let tags: &[(bool, u8)] = &[$(RawTag::neg_val($itag)),*, $(RawTag::neg_val($gtag)),*];
let mut mask_false: u8 = 0;
let mut mask_true: u8 = 0;
let mut i = 0;
while i < tags.len() {
#[expect(clippy::indexing_slicing, reason = "loop condition guarantees `i < tags.len()`")]
let (neg, val) = tags[i];
let bit = 1 << val;
if neg {
assert!(mask_true & bit == 0, "duplicate true tag id");
mask_true |= bit;
} else {
assert!(mask_false & bit == 0, "duplicate false tag id");
mask_false |= bit;
}
i += 1;
}
};
// 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> {
const NEEDS_TRACE: bool = true;
fn trace<T: Trace<'gc>>(&self, cc: &mut T) {
let Some(tag) = self.raw.tag() else { return };
match tag {
$($gtag => unsafe {
// SAFETY: `tag` matched `$gtag`, so `downcast` to the
// corresponding GC type is guaranteed to be `Some`.
self.downcast::<$gtype>().unwrap_unchecked().trace(cc)
},)*
$($itag => (),)*
_ => unreachable!("invalid value tag"),
}
}
}
impl fmt::Debug for Value<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
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 {
self.raw.float().unwrap_unchecked()
}),
// SAFETY: `tag()` matched `$itag`, so `downcast` to the
// corresponding inline type is guaranteed to be `Some`.
$(Some($itag) => write!(f, "{}({:?})", $iname, unsafe {
self.downcast::<$itype>().unwrap_unchecked()
}),)*
$(Some($gtag) =>
write!(f, "{}(..)", $gname),)*
_ => unreachable!("invalid value tag"),
}
}
}
};
}
define_value_types! {
inline {
i32 => RawTag::P1, NixType::Int, "SmallInt";
bool => RawTag::P2, NixType::Bool, "Bool";
Null => RawTag::P3, NixType::Null, "Null";
StringId => RawTag::P4, NixType::String, "SmallString";
PrimOp => RawTag::P5, NixType::PrimOp, "PrimOp";
Path => RawTag::P6, NixType::Path, "Path";
}
gc {
i64 => RawTag::P7, NixType::Int, "BigInt";
NixString => RawTag::N1, NixType::String, "String";
AttrSet<'_> => RawTag::N2, NixType::AttrSet, "AttrSet";
List<'_> => RawTag::N3, NixType::List, "List";
Thunk<'_> => RawTag::N4, NixType::Thunk, "Thunk";
Closure<'_> => RawTag::N5, NixType::Closure, "Closure";
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)
}
}
/// # Nix runtime value representation
///
/// NaN-boxed value fitting in 8 bytes.
#[derive(Copy, Clone)]
#[repr(transparent)]
pub struct Value<'gc> {
raw: RawBox,
_marker: PhantomData<Gc<'gc, ()>>,
}
impl Default for Value<'_> {
#[inline(always)]
fn default() -> Self {
Self::new(Null)
}
}
impl<'gc> Value<'gc> {
#[inline(always)]
const fn tag(self) -> Option<RawTag> {
self.raw.tag()
}
}
impl<'gc> Value<'gc> {
#[inline]
#[expect(
private_bounds,
reason = "Storable is a sealed implementation detail of the value system"
)]
pub fn new<T: Storable<'gc>>(val: T) -> Self {
Self {
raw: val.to_raw_box(),
_marker: PhantomData,
}
}
#[inline]
pub fn make_int(val: i64, mc: &Mutation<'gc>) -> Self {
if val >= i32::MIN as i64 && val <= i32::MAX as i64 {
Value::new(val as i32)
} else {
Value::new(Gc::new(mc, val))
}
}
#[inline]
pub fn is<T: ValueVariant<'gc>>(self) -> bool {
T::Ty::is(&self.raw)
}
#[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]
pub fn to_bits(self) -> u64 {
self.raw.to_bits()
}
#[inline]
pub fn downcast_num(self) -> Option<NixNum> {
if let Some(i) = self.downcast::<i32>() {
Some(NixNum::Int(i as i64))
} else if let Some(gc_i) = self.downcast::<i64>() {
Some(NixNum::Int(*gc_i))
} else {
self.downcast::<f64>().map(NixNum::Float)
}
}
#[inline]
pub fn restrict(self) -> Result<StrictValue<'gc>, Gc<'gc, Thunk<'gc>>> {
if let Some(thunk) = self.downcast::<Thunk>() {
Err(thunk)
} else {
Ok(StrictValue(self))
}
}
#[inline]
#[expect(
clippy::unreachable,
reason = "the preceding `if`/`else if` chain exhausts every registered value tag"
)]
pub fn ty(self) -> NixType {
if self.is::<f64>() {
NixType::Float
} else if self.is::<i32>() || self.is::<i64>() {
NixType::Int
} else if self.is::<bool>() {
NixType::Bool
} else if self.is::<Null>() {
NixType::Null
} else if self.is::<StringId>() {
NixType::String
} else if self.is::<PrimOp>() {
NixType::PrimOp
} else if self.is::<NixString>() {
NixType::String
} else if self.is::<Path>() {
NixType::Path
} else if self.is::<AttrSet>() {
NixType::AttrSet
} else if self.is::<List>() {
NixType::List
} else if self.is::<Thunk>() {
NixType::Thunk
} else if self.is::<Closure>() {
NixType::Closure
} else if self.is::<PrimOpApp>() {
NixType::PrimOpApp
} else {
unreachable!("value has no recognized type tag")
}
}
#[inline]
pub fn expect<T: ValueVariant<'gc>>(self) -> Result<T::Ty, NixType> {
self.downcast::<T>().ok_or_else(|| self.ty())
}
#[inline]
pub fn expect_num(self) -> Result<NixNum, NixType> {
self.downcast_num().ok_or_else(|| self.ty())
}
}
#[derive(Copy, Clone, Default)]
#[repr(transparent)]
pub struct StaticValue(Value<'static>);
impl<'gc> From<StaticValue> for Value<'gc> {
#[inline]
fn from(value: StaticValue) -> Self {
// SAFETY: StaticValue is guaranteed to not contain any `Gc`.
unsafe { std::mem::transmute::<Value<'static>, Value<'gc>>(value.0) }
}
}
impl StaticValue {
#[inline]
#[expect(
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]
pub fn is<T: ValueVariant<'static>>(self) -> bool {
self.0.is::<T>()
}
#[inline]
pub fn downcast<T: ValueVariant<'static>>(self) -> Option<T::Ty> {
self.0.downcast::<T>()
}
#[inline]
pub fn to_bits(self) -> u64 {
self.0.raw.to_bits()
}
}
#[derive(Clone, Copy, Debug)]
pub struct Null;
impl RawStore for Null {
fn to_val(self, value: &mut RawValue) {
value.set_data([0; 6]);
}
fn from_val(_: &RawValue) -> Self {
Self
}
}
impl RawStore for StringId {
fn to_val(self, value: &mut RawValue) {
(self.0.to_usize() as u32).to_val(value);
}
fn from_val(value: &RawValue) -> Self {
Self(
SymbolU32::try_from_usize(u32::from_val(value) as usize)
.expect("failed to read StringId from Value"),
)
}
}
/// A canonicalized absolute path. Inline value carrying an interned
/// `StringId` whose contents are the path's absolute, dot-resolved form.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Path(pub StringId);
impl RawStore for Path {
fn to_val(self, value: &mut RawValue) {
self.0.to_val(value);
}
fn from_val(value: &RawValue) -> Self {
Self(StringId::from_val(value))
}
}
#[derive(Collect)]
#[collect(require_static)]
pub struct NixString {
data: Box<str>,
context: StringContext,
}
impl NixString {
pub fn new(s: impl Into<Box<str>>) -> Self {
Self {
data: s.into(),
context: StringContext::new(),
}
}
/// Construct a `NixString` whose `context` is already sorted+deduped.
/// The caller is responsible for invariant maintenance.
pub fn with_context(s: impl Into<Box<str>>, context: StringContext) -> Self {
Self {
data: s.into(),
context,
}
}
pub fn as_str(&self) -> &str {
&self.data
}
pub fn context(&self) -> &StringContext {
&self.context
}
pub fn has_context(&self) -> bool {
!self.context.is_empty()
}
}
impl fmt::Debug for NixString {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Debug::fmt(&self.data, f)
}
}
#[derive(Collect, Debug, Default)]
#[collect(no_drop)]
pub struct AttrSet<'gc> {
pub entries: SmallVec<[(StringId, Value<'gc>); 4]>,
}
impl<'gc> AttrSet<'gc> {
pub fn from_sorted_unchecked(entries: SmallVec<[(StringId, Value<'gc>); 4]>) -> Self {
debug_assert!(entries.is_sorted_by_key(|(key, _)| *key));
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>> {
self.entries
.binary_search_by_key(&key, |(k, _)| *k)
.ok()
.map(|i| self.entries[i].1)
}
pub fn has(&self, key: StringId) -> bool {
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> {
use std::cmp::Ordering::*;
debug_assert!(self.entries.is_sorted_by_key(|(key, _)| *key));
debug_assert!(other.entries.is_sorted_by_key(|(key, _)| *key));
let mut entries = SmallVec::new();
let mut i = 0;
let mut j = 0;
while i < self.entries.len() && j < other.entries.len() {
match self.entries[i].0.cmp(&other.entries[j].0) {
Less => {
entries.push(self.entries[i]);
i += 1;
}
Greater => {
entries.push(other.entries[j]);
j += 1;
}
Equal => {
entries.push(other.entries[j]);
i += 1;
j += 1;
}
}
}
entries.extend(other.entries[j..].iter().cloned());
entries.extend(self.entries[i..].iter().cloned());
debug_assert!(entries.is_sorted_by_key(|(key, _)| *key));
Gc::new(mc, AttrSet { entries })
}
}
#[derive(Collect, Debug, Default)]
#[repr(transparent)]
#[collect(no_drop)]
pub struct List<'gc> {
pub inner: RefLock<SmallVec<[Value<'gc>; 4]>>,
}
impl<'gc> List<'gc> {
pub fn new(mc: &Mutation<'gc>, data: SmallVec<[Value<'gc>; 4]>) -> Gc<'gc, Self> {
Gc::new(
mc,
Self {
inner: RefLock::new(data),
},
)
}
pub fn new_gc(mc: &Mutation<'gc>) -> Gc<'gc, Self> {
Gc::new(mc, Self::default())
}
}
impl<'gc> Unlock for List<'gc> {
type Unlocked = RefCell<SmallVec<[Value<'gc>; 4]>>;
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() }
}
}
pub type Thunk<'gc> = RefLock<ThunkState<'gc>>;
#[derive(Collect, Debug)]
#[collect(no_drop)]
pub enum ThunkState<'gc> {
Pending { ip: usize, env: GcEnv<'gc> },
Apply { func: Value<'gc>, arg: Value<'gc> },
Blackhole,
Evaluated(StrictValue<'gc>),
}
#[derive(Collect, Debug)]
#[collect(no_drop)]
pub struct Env<'gc> {
pub locals: SmallVec<[Value<'gc>; 4]>,
pub prev: Option<GcEnv<'gc>>,
}
pub type GcEnv<'gc> = GcRefLock<'gc, Env<'gc>>;
impl<'gc> Env<'gc> {
pub fn empty() -> Self {
Env {
locals: SmallVec::new(),
prev: None,
}
}
#[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 {
let mut locals = smallvec::smallvec![Value::default(); 1 + n_locals as usize];
locals[0] = arg;
Env {
locals,
prev: Some(prev),
}
}
}
#[derive(Collect, Debug)]
#[collect(no_drop)]
pub struct Closure<'gc> {
pub ip: u32,
pub n_locals: u32,
pub env: Gc<'gc, RefLock<Env<'gc>>>,
pub pattern: Option<Gc<'gc, PatternInfo>>,
}
#[derive(Collect, Debug)]
#[collect(require_static)]
pub struct PatternInfo {
pub required: SmallVec<[StringId; 4]>,
pub optional: SmallVec<[StringId; 4]>,
pub ellipsis: bool,
pub param_spans: Box<[(StringId, u32)]>,
}
#[repr(packed, Rust)]
#[derive(Clone, Copy, Debug, Collect)]
#[collect(require_static)]
pub struct PrimOp {
pub id: BuiltinId,
pub arity: u8,
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 {
fn to_val(self, value: &mut RawValue) {
let bytes = self.dispatch_ip.to_le_bytes();
value.set_data([
self.id as u8,
self.arity,
bytes[0],
bytes[1],
bytes[2],
bytes[3],
]);
}
fn from_val(value: &RawValue) -> Self {
let [id, arity, bytes @ ..] = *value.data();
Self {
id: BuiltinId::try_from(id).expect("invalid BuiltinId"),
arity,
dispatch_ip: u32::from_le_bytes(bytes),
}
}
}
#[derive(Collect, Debug)]
#[collect(no_drop)]
pub struct PrimOpApp<'gc> {
pub primop: PrimOp,
pub arity: u8,
pub args: [Value<'gc>; 3],
}
#[derive(Copy, Clone, Default, Collect)]
#[repr(transparent)]
#[collect(no_drop)]
pub struct StrictValue<'gc>(Value<'gc>);
impl<'gc> StrictValue<'gc> {
#[inline]
pub fn relax(self) -> Value<'gc> {
self.0
}
}
impl<'gc> Deref for StrictValue<'gc> {
type Target = Value<'gc>;
#[inline]
fn deref(&self) -> &Value<'gc> {
&self.0
}
}
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<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Debug::fmt(&self.0, f)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Collect)]
#[collect(require_static)]
pub enum NixType {
Int,
Float,
Bool,
Null,
String,
Path,
AttrSet,
List,
Thunk,
Closure,
PrimOp,
PrimOpApp,
}
impl NixType {
pub fn display(self) -> &'static str {
use NixType::*;
match self {
Int => "an integer",
Float => "a float",
Bool => "a boolean",
Null => "null",
String => "a string",
Path => "a path",
AttrSet => "a set",
List => "a list",
Thunk => "a thunk",
Closure => "a function",
PrimOp => "a built-in function",
PrimOpApp => "a partially applied built-in function",
}
}
}
impl std::fmt::Display for NixType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.display())
}
}
pub enum NixNum {
Int(i64),
Float(f64),
}
-22
View File
@@ -1,22 +0,0 @@
[package]
name = "fix-vm"
version = "0.1.0"
edition = "2024"
[dependencies]
gc-arena = { workspace = true }
hashbrown = { workspace = true }
smallvec = { workspace = true }
sysinfo = { version = "0.39", default-features = false, features = ["system"] }
fix-bytecode = { path = "../fix-bytecode" }
fix-error = { path = "../fix-error" }
fix-lang = { path = "../fix-lang" }
fix-macros = { path = "../fix-macros" }
fix-runtime = { path = "../fix-runtime" }
[features]
tailcall = []
[lints]
workspace = true
-309
View File
@@ -1,309 +0,0 @@
#![cfg(feature = "tailcall")]
use gc_arena::Mutation;
use crate::{Break, BytecodeReader, Step, Vm, VmRuntimeCtx};
pub(crate) enum TailResult {
YieldFuel(u32),
Done,
LoadFile,
}
pub(crate) type OpFn<'gc, C> = extern "rust-preserve-none" fn(
&mut Vm<'gc>,
&Mutation<'gc>,
&mut C,
&[u8],
&DispatchTable<'gc, C>,
u32,
u32,
) -> TailResult;
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>(
_vm: &mut Vm<'gc>,
_mc: &Mutation<'gc>,
_ctx: &mut C,
_bc: &[u8],
_table: &DispatchTable<'gc, C>,
pc: u32,
_fuel: u32,
) -> TailResult {
panic!("illegal opcode at pc = {pc}");
}
macro_rules! tail_dispatch_after {
($result:expr, $new_pc:expr, $vm:ident, $mc:ident, $ctx:ident, $bc:ident, $table:ident, $fuel:ident) => {{
match $result {
Step::Continue(()) | Step::Break(Break::Force) => {}
Step::Break(Break::LoadFile) => return TailResult::LoadFile,
Step::Break(Break::Done) => return TailResult::Done,
}
let new_pc: u32 = $new_pc;
if $fuel == 0 {
return TailResult::YieldFuel(new_pc);
}
let next_op = $bc[new_pc as usize] as usize;
become $table.0[next_op]($vm, $mc, $ctx, $bc, $table, new_pc, $fuel - 1)
}};
}
macro_rules! tail_fn {
($name:ident, ()) => {
extern "rust-preserve-none" fn $name<'gc, C: VmRuntimeCtx>(
vm: &mut Vm<'gc>,
mc: &Mutation<'gc>,
ctx: &mut C,
bc: &[u8],
table: &DispatchTable<'gc, C>,
pc: u32,
fuel: u32,
) -> TailResult {
let result = crate::instructions::$name(vm);
tail_dispatch_after!(result, pc + 1, vm, mc, ctx, bc, table, fuel)
}
};
($name:ident, (reader)) => {
extern "rust-preserve-none" fn $name<'gc, C: VmRuntimeCtx>(
vm: &mut Vm<'gc>,
mc: &Mutation<'gc>,
ctx: &mut C,
bc: &[u8],
table: &DispatchTable<'gc, C>,
pc: u32,
fuel: u32,
) -> TailResult {
let mut reader = BytecodeReader::from_after_op(bc, pc as usize);
let result = crate::instructions::$name(vm, &mut reader);
tail_dispatch_after!(result, reader.pc() as u32, vm, mc, ctx, bc, table, fuel)
}
};
($name:ident, (reader, mc)) => {
extern "rust-preserve-none" fn $name<'gc, C: VmRuntimeCtx>(
vm: &mut Vm<'gc>,
mc: &Mutation<'gc>,
ctx: &mut C,
bc: &[u8],
table: &DispatchTable<'gc, C>,
pc: u32,
fuel: u32,
) -> TailResult {
let mut reader = BytecodeReader::from_after_op(bc, pc as usize);
let result = crate::instructions::$name(vm, &mut reader, mc);
tail_dispatch_after!(result, reader.pc() as u32, vm, mc, ctx, bc, table, fuel)
}
};
($name:ident, (ctx, reader, mc)) => {
extern "rust-preserve-none" fn $name<'gc, C: VmRuntimeCtx>(
vm: &mut Vm<'gc>,
mc: &Mutation<'gc>,
ctx: &mut C,
bc: &[u8],
table: &DispatchTable<'gc, C>,
pc: u32,
fuel: u32,
) -> TailResult {
let mut reader = BytecodeReader::from_after_op(bc, pc as usize);
let result = crate::instructions::$name(vm, ctx, &mut reader, mc);
tail_dispatch_after!(result, reader.pc() as u32, vm, mc, ctx, bc, table, fuel)
}
};
($name:ident, (ctx)) => {
extern "rust-preserve-none" fn $name<'gc, C: VmRuntimeCtx>(
vm: &mut Vm<'gc>,
mc: &Mutation<'gc>,
ctx: &mut C,
bc: &[u8],
table: &DispatchTable<'gc, C>,
pc: u32,
fuel: u32,
) -> TailResult {
let result = crate::instructions::$name(vm, ctx);
tail_dispatch_after!(result, pc + 1, vm, mc, ctx, bc, table, fuel)
}
};
}
tail_fn!(op_push_smi, (reader));
tail_fn!(op_push_bigint, (reader, mc));
tail_fn!(op_push_float, (reader));
tail_fn!(op_push_string, (reader));
tail_fn!(op_push_null, ());
tail_fn!(op_push_true, ());
tail_fn!(op_push_false, ());
tail_fn!(op_load_local, (reader));
tail_fn!(op_load_outer, (reader));
tail_fn!(op_store_local, (reader, mc));
tail_fn!(op_alloc_locals, (reader, mc));
tail_fn!(op_make_thunk, (reader, mc));
tail_fn!(op_make_closure, (reader, mc));
tail_fn!(op_make_pattern_closure, (reader, mc));
tail_fn!(op_call, (ctx, reader, mc));
tail_fn!(op_dispatch_cont, (ctx, reader, mc));
tail_fn!(op_return, (ctx, reader, mc));
tail_fn!(op_make_attrs, (ctx, reader, mc));
tail_fn!(op_make_empty_attrs, ());
tail_fn!(op_select_static, (ctx, reader, mc));
tail_fn!(op_select_dynamic, (ctx, reader, mc));
tail_fn!(op_has_attr_path_static, (ctx, reader, mc));
tail_fn!(op_has_attr_path_dynamic, (ctx, reader, mc));
tail_fn!(op_jump_if_select_failed, (reader));
tail_fn!(op_jump_if_select_succeeded, (reader));
tail_fn!(op_has_attr_static, (reader, mc));
tail_fn!(op_has_attr_dynamic, (ctx, reader, mc));
tail_fn!(op_has_attr_resolve, ());
tail_fn!(op_make_list, (ctx, reader, mc));
tail_fn!(op_make_empty_list, ());
tail_fn!(op_add, (ctx, reader, mc));
tail_fn!(op_sub, (reader, mc));
tail_fn!(op_mul, (reader, mc));
tail_fn!(op_div, (reader, mc));
tail_fn!(op_eq, (ctx, reader, mc));
tail_fn!(op_neq, (ctx, reader, mc));
tail_fn!(op_lt, (ctx, reader, mc));
tail_fn!(op_gt, (ctx, reader, mc));
tail_fn!(op_leq, (ctx, reader, mc));
tail_fn!(op_geq, (ctx, reader, mc));
tail_fn!(op_concat, (reader, mc));
tail_fn!(op_update, (reader, mc));
tail_fn!(op_neg, (reader, mc));
tail_fn!(op_not, (reader, mc));
tail_fn!(op_jump_if_false, (reader, mc));
tail_fn!(op_jump_if_true, (reader, mc));
tail_fn!(op_jump, (reader));
tail_fn!(op_coerce_to_string, (reader, mc));
tail_fn!(op_concat_strings, (ctx, reader, mc));
tail_fn!(op_resolve_path, (ctx, reader, mc));
tail_fn!(op_assert, (ctx, reader, mc));
tail_fn!(op_lookup_with, (ctx, reader, mc));
tail_fn!(op_load_builtins, ());
tail_fn!(op_load_builtin, (reader));
tail_fn!(op_load_repl_binding, (reader));
tail_fn!(op_load_scoped_binding, (ctx, reader, mc));
macro_rules! table {
($($variant:ident => $fn:ident),* $(,)?) => {
impl<'gc, C: VmRuntimeCtx> DispatchTable<'gc, C> {
#[expect(clippy::indexing_slicing, reason = "Op is repr(u8)")]
pub(crate) const NEW: Self = {
let mut arr: [OpFn<'gc, C>; 256] = [op_illegal; 256];
$( arr[fix_bytecode::Op::$variant as usize] = $fn; )*
DispatchTable(arr)
};
}
// Exhaustiveness check: fails to compile if `fix_bytecode::Op` gains,
// loses, or renames a variant that isn't wired up above.
const _: fn(fix_bytecode::Op) = |op| match op {
$( fix_bytecode::Op::$variant => (), )*
};
};
}
table! {
PushSmi => op_push_smi,
PushBigInt => op_push_bigint,
PushFloat => op_push_float,
PushString => op_push_string,
PushNull => op_push_null,
PushTrue => op_push_true,
PushFalse => op_push_false,
LoadLocal => op_load_local,
LoadOuter => op_load_outer,
StoreLocal => op_store_local,
AllocLocals => op_alloc_locals,
MakeThunk => op_make_thunk,
MakeClosure => op_make_closure,
MakePatternClosure => op_make_pattern_closure,
Call => op_call,
DispatchCont => op_dispatch_cont,
Return => op_return,
MakeAttrs => op_make_attrs,
MakeEmptyAttrs => op_make_empty_attrs,
SelectStatic => op_select_static,
SelectDynamic => op_select_dynamic,
HasAttrPathStatic => op_has_attr_path_static,
HasAttrPathDynamic => op_has_attr_path_dynamic,
HasAttrStatic => op_has_attr_static,
HasAttrDynamic => op_has_attr_dynamic,
HasAttrResolve => op_has_attr_resolve,
JumpIfSelectSucceeded => op_jump_if_select_succeeded,
JumpIfSelectFailed => op_jump_if_select_failed,
MakeList => op_make_list,
MakeEmptyList => op_make_empty_list,
OpAdd => op_add,
OpSub => op_sub,
OpMul => op_mul,
OpDiv => op_div,
OpEq => op_eq,
OpNeq => op_neq,
OpLt => op_lt,
OpGt => op_gt,
OpLeq => op_leq,
OpGeq => op_geq,
OpConcat => op_concat,
OpUpdate => op_update,
OpNeg => op_neg,
OpNot => op_not,
JumpIfFalse => op_jump_if_false,
JumpIfTrue => op_jump_if_true,
Jump => op_jump,
CoerceToString => op_coerce_to_string,
ConcatStrings => op_concat_strings,
ResolvePath => op_resolve_path,
Assert => op_assert,
LookupWith => op_lookup_with,
LoadBuiltins => op_load_builtins,
LoadBuiltin => op_load_builtin,
LoadReplBinding => op_load_repl_binding,
LoadScopedBinding => op_load_scoped_binding,
Illegal => op_illegal,
}
#[expect(
clippy::indexing_slicing,
reason = "assume well-formed bytecode; Op is repr(u8)"
)]
pub(crate) fn run_tailcall<'gc, C: VmRuntimeCtx>(
vm: &mut Vm<'gc>,
mc: &Mutation<'gc>,
ctx: &mut C,
bc: &[u8],
pc: u32,
) -> TailResult {
let table = &DispatchTable::<'gc, C>::NEW;
let op = bc[pc as usize] as usize;
table.0[op](vm, mc, ctx, bc, table, pc, Vm::DEFAULT_FUEL_AMOUNT)
}
-315
View File
@@ -1,315 +0,0 @@
use std::cmp::Ordering;
use fix_runtime::*;
use gc_arena::{Gc, Mutation, RefLock};
use crate::{BytecodeReader, NixNum, Step, VmError, VmRuntimeCtx};
#[inline(always)]
pub(crate) fn op_add<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let (lhs, rhs) = m.force_and_retry::<(StrictValue, StrictValue)>(reader, mc)?;
// if the LHS is a path, the result is a path obtained by
// canonicalizing the concatenated string. RHS may be a path or a
// string. (A `string + path` keeps the string-typed result, handled
// by the next branch.)
if lhs.is::<Path>() {
let (Some(ls), Some(rs)) = (ctx.get_string_or_path(lhs), ctx.get_string_or_path(rhs))
else {
return m.finish_err(fix_error::Error::eval_error(format!(
"cannot append {} to a path",
rhs.ty()
)));
};
let combined = format!("{ls}{rs}");
let canon = canon_path_str(&combined);
let sid = ctx.intern_string(canon);
m.push(Value::new(fix_runtime::Path(sid)));
return Step::Continue(());
}
if let (Some(ls), Some(rs)) = (ctx.get_string(lhs), ctx.get_string_or_path(rhs)) {
let merged = ctx
.get_string_context(lhs)
.merge(ctx.get_string_context(rhs));
let ns = Gc::new(
mc,
crate::NixString::with_context(format!("{ls}{rs}"), merged),
);
m.push(Value::new(ns));
return Step::Continue(());
}
let res = numeric_binop(lhs, rhs, mc, i64::wrapping_add, |a, b| a + b);
match res {
Ok(val) => {
m.push(val);
Step::Continue(())
}
Err(e) => m.finish_vm_err(e),
}
}
#[inline(always)]
pub(crate) fn op_sub<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
op_arith(m, reader, mc, i64::wrapping_sub, |a, b| a - b)
}
#[inline(always)]
pub(crate) fn op_mul<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
op_arith(m, reader, mc, i64::wrapping_mul, |a, b| a * b)
}
#[inline(always)]
fn op_arith<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
int_op: fn(i64, i64) -> i64,
float_op: fn(f64, f64) -> f64,
) -> Step {
let (lhs, rhs) = m.force_and_retry::<(StrictValue, StrictValue)>(reader, mc)?;
let res = numeric_binop(lhs, rhs, mc, int_op, float_op);
match res {
Ok(val) => {
m.push(val);
Step::Continue(())
}
Err(e) => m.finish_vm_err(e),
}
}
#[inline(always)]
pub(crate) fn op_div<'gc, M: Machine<'gc>>(
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)) {
(_, Some(NixNum::Int(0))) | (_, Some(NixNum::Float(0.))) => {
return m.finish_vm_err(VmError::Uncatchable(fix_error::Error::eval_error(
"division by zero",
)));
}
_ => {}
}
let res = numeric_binop(lhs, rhs, mc, |a, b| a / b, |a, b| a / b);
match res {
Ok(val) => {
m.push(val);
Step::Continue(())
}
Err(e) => m.finish_vm_err(e),
}
}
#[inline(always)]
pub(crate) fn op_eq<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let (lhs, rhs) = m.force_and_retry::<(StrictValue, StrictValue)>(reader, mc)?;
crate::primops::start_eq(m, ctx, reader, mc, lhs, rhs, false)
}
#[inline(always)]
pub(crate) fn op_neq<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let (lhs, rhs) = m.force_and_retry::<(StrictValue, StrictValue)>(reader, mc)?;
crate::primops::start_eq(m, ctx, reader, mc, lhs, rhs, true)
}
#[inline(always)]
pub(crate) fn op_lt<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
compare_values(m, ctx, reader, mc, Ordering::is_lt)
}
#[inline(always)]
pub(crate) fn op_gt<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
compare_values(m, ctx, reader, mc, Ordering::is_gt)
}
#[inline(always)]
pub(crate) fn op_leq<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
compare_values(m, ctx, reader, mc, Ordering::is_le)
}
#[inline(always)]
pub(crate) fn op_geq<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
compare_values(m, ctx, reader, mc, Ordering::is_ge)
}
fn compare_values<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
pred: fn(Ordering) -> bool,
) -> Step {
let (lhs, rhs) = m.force_and_retry::<(StrictValue, StrictValue)>(reader, mc)?;
match compare_values_inner(m, ctx, pred, lhs, rhs) {
Ok(()) => Step::Continue(()),
Err(e) => m.finish_vm_err(e),
}
}
#[inline(always)]
pub(crate) fn op_concat<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let (l, r) = m.force_and_retry::<(Gc<List>, Gc<List>)>(reader, mc)?;
let mut items = smallvec::SmallVec::new();
items.extend_from_slice(&l.inner.borrow());
items.extend_from_slice(&r.inner.borrow());
m.push(Value::new(Gc::new(
mc,
crate::List {
inner: RefLock::new(items),
},
)));
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_update<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let (l, r) = m.force_and_retry::<(Gc<AttrSet>, Gc<AttrSet>)>(reader, mc)?;
m.push(Value::new(l.merge(&r, mc)));
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_neg<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let rhs = m.force_and_retry::<NixNum>(reader, mc)?;
match rhs {
NixNum::Int(int) => m.push(Value::make_int(-int, mc)),
NixNum::Float(float) => m.push(Value::new(-float)),
}
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_not<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let rhs = m.force_and_retry::<bool>(reader, mc)?;
m.push(Value::new(!rhs));
Step::Continue(())
}
fn compare_values_inner<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &impl VmRuntimeCtx,
pred: fn(Ordering) -> bool,
lhs: StrictValue<'gc>,
rhs: StrictValue<'gc>,
) -> crate::VmResult<()> {
if let (Some(a), Some(b)) = (get_num(lhs), get_num(rhs)) {
let ord = match (a, b) {
(NixNum::Int(a), NixNum::Int(b)) => a.cmp(&b),
(NixNum::Float(a), NixNum::Float(b)) => a.partial_cmp(&b).unwrap_or(Ordering::Less),
(NixNum::Int(a), NixNum::Float(b)) => {
(a as f64).partial_cmp(&b).unwrap_or(Ordering::Less)
}
(NixNum::Float(a), NixNum::Int(b)) => {
a.partial_cmp(&(b as f64)).unwrap_or(Ordering::Less)
}
};
m.push(Value::new(pred(ord)));
return Ok(());
}
if let (Some(a), Some(b)) = (ctx.get_string(lhs), ctx.get_string(rhs)) {
m.push(Value::new(pred(a.cmp(b))));
return Ok(());
}
if let (Some(a), Some(b)) = (lhs.downcast::<Path>(), rhs.downcast::<Path>()) {
let a = ctx.resolve_string(a.0);
let b = ctx.resolve_string(b.0);
m.push(Value::new(pred(a.cmp(b))));
return Ok(());
}
// TODO: compare other types
Err(crate::vm_err(format!(
"cannot compare {} with {}",
lhs.ty(),
rhs.ty()
)))
}
pub(crate) fn get_num(val: StrictValue<'_>) -> Option<NixNum> {
if let Some(i) = val.downcast::<i32>() {
Some(NixNum::Int(i64::from(i)))
} else if let Some(gc_i) = val.downcast::<i64>() {
Some(NixNum::Int(*gc_i))
} else {
val.downcast::<f64>().map(NixNum::Float)
}
}
#[inline]
fn numeric_binop<'gc>(
lhs: StrictValue<'gc>,
rhs: StrictValue<'gc>,
mc: &Mutation<'gc>,
int_op: fn(i64, i64) -> i64,
float_op: fn(f64, f64) -> f64,
) -> crate::VmResult<Value<'gc>> {
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::Float(a)), Some(NixNum::Float(b))) => Ok(Value::new(float_op(a, b))),
(Some(NixNum::Int(a)), Some(NixNum::Float(b))) => Ok(Value::new(float_op(a as f64, b))),
(Some(NixNum::Float(a)), Some(NixNum::Int(b))) => Ok(Value::new(float_op(a, b as f64))),
_ => Err(crate::vm_err(format!(
"cannot perform arithmetic on non-numbers: {:?}",
(lhs.ty(), rhs.ty())
))),
}
}
-195
View File
@@ -1,195 +0,0 @@
use fix_bytecode::Continuation;
use fix_error::Error;
use fix_runtime::{resolve_operand, *};
use gc_arena::{Gc, Mutation, RefLock};
use crate::{
BytecodeReader, CallFrame, Closure, Env, ForceMode, Step, ThunkState, VmRuntimeCtx,
VmRuntimeCtxExt,
};
#[inline(always)]
#[expect(
clippy::indexing_slicing,
reason = "app.args is a fixed [Value; 3]; indices are bounded by primop arity (<= 3) validated at PrimOpApp construction"
)]
pub(crate) fn call<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
arg: Value<'gc>,
resume_pc: usize,
) -> Step {
let func = m.force_and_retry::<StrictValue>(reader, mc)?;
if m.call_depth() > 10000 {
return m.finish_err(Error::eval_error("stack overflow; max-call-depth exceeded"));
}
m.inc_call_depth();
if let Some(closure) = func.downcast::<Closure>() {
if closure.pattern.is_some() {
// FIXME: better DX...
m.push(func.relax());
m.push(arg);
m.push_call_frame(CallFrame {
pc: resume_pc,
thunk: None,
env: m.env(),
depth: None,
});
reader.set_pc(Continuation::CallPattern.ip() as usize);
return Step::Continue(());
}
let ip = closure.ip;
let n_locals = closure.n_locals;
let env = closure.env;
let new_env = Gc::new(mc, RefLock::new(Env::with_arg(arg, n_locals, env)));
m.push_call_frame(CallFrame {
pc: resume_pc,
thunk: None,
env: m.env(),
depth: None,
});
reader.set_pc(ip as usize);
m.set_env(new_env);
} else if let Some(primop) = func.downcast::<PrimOp>() {
if primop.arity == 1 {
m.push(arg);
m.push_call_frame(CallFrame {
pc: resume_pc,
thunk: None,
env: m.env(),
depth: None,
});
reader.set_pc(primop.dispatch_ip as usize)
} else {
let app = PrimOpApp {
primop,
arity: primop.arity - 1,
args: [arg, Value::default(), Value::default()],
};
m.push(Value::new(Gc::new(mc, app)));
}
} else if let Some(app) = func.downcast::<PrimOpApp>() {
if app.arity == 1 {
for i in 0..app.primop.arity - 1 {
m.push(app.args[i as usize]);
}
m.push(arg);
m.push_call_frame(CallFrame {
pc: resume_pc,
thunk: None,
env: m.env(),
depth: None,
});
reader.set_pc(app.primop.dispatch_ip as usize)
} else {
let position = (app.primop.arity - app.arity) as usize;
let mut new_app = PrimOpApp {
arity: app.arity - 1,
..*app
};
new_app.args[position] = arg;
m.push(Value::new(Gc::new(mc, new_app)))
}
} else if let Some(attrs) = func.downcast::<AttrSet>()
&& let Some(functor) = attrs.lookup(m.functor_sym())
{
// f arg => (f.__functor f) arg
//
// Stage the work for `CallFunctor1` so retries during force are
// safe: the stack invariant `[..., orig_arg, self, functor]`
// holds every time control re-enters phase 1.
m.dec_call_depth();
m.push_call_frame(CallFrame {
pc: resume_pc,
thunk: None,
env: m.env(),
depth: None,
});
m.push(arg);
m.push(func.relax());
m.push(functor);
reader.set_pc(Continuation::CallFunctor1.ip() as usize);
return Step::Continue(());
} else {
return m.finish_err(Error::eval_error(format!(
"attempt to call something which is not a function but {}",
func.ty()
)));
}
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_call<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let arg = resolve_operand(&reader.read_operand_data(), mc, ctx, m);
let pc = reader.pc();
m.call(reader, mc, arg, pc)
}
#[inline(always)]
pub(crate) fn op_return<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let val = m.force_and_retry::<StrictValue>(reader, mc)?;
let Some(CallFrame {
pc: ret_pc,
thunk,
env,
depth,
}) = m.pop_call_frame()
else {
match m.force_mode() {
ForceMode::AsIs => return m.finish_ok(ctx.convert_value(val.relax())),
ForceMode::Shallow => {
m.push(val.relax());
reader.set_pc(Continuation::ForceResultShallow.ip() as usize);
return Step::Continue(());
}
ForceMode::Deep => {
m.push(val.relax());
m.push(val.relax());
m.push_call_frame(CallFrame {
pc: Continuation::ForceResultDeepFinish.ip() as usize,
thunk: None,
env: m.env(),
depth: None,
});
m.inc_call_depth();
reader.set_pc(Continuation::PDeepSeq0.ip() as usize);
return Step::Continue(());
}
}
};
reader.set_pc(ret_pc);
if let Some(outer_thunk) = thunk {
*outer_thunk.borrow_mut(mc) = ThunkState::Evaluated(val);
if let Some(depth) = depth {
m.replace(depth, val.relax());
}
} else {
m.dec_call_depth();
m.push(val.relax())
}
m.set_env(env);
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_dispatch_cont<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
crate::primops::dispatch_cont(m, ctx, reader, mc)
}
-93
View File
@@ -1,93 +0,0 @@
use fix_runtime::Machine;
use gc_arena::{Gc, Mutation, RefLock};
use crate::{BytecodeReader, Step, ThunkState, Value};
#[inline(always)]
pub(crate) fn op_make_thunk<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let entry_point = reader.read_u32();
let thunk = Gc::new(
mc,
RefLock::new(ThunkState::Pending {
ip: entry_point as usize,
env: m.env(),
}),
);
m.push(Value::new(thunk));
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_make_closure<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let entry_point = reader.read_u32();
let n_locals = reader.read_u32();
let closure = Gc::new(
mc,
crate::Closure {
ip: entry_point,
n_locals,
env: m.env(),
pattern: None,
},
);
m.push(Value::new(closure));
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_make_pattern_closure<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let entry_point = reader.read_u32();
let n_locals = reader.read_u32();
let req_count = reader.read_u16() as usize;
let opt_count = reader.read_u16() as usize;
let has_ellipsis = reader.read_u8() != 0;
let mut required = smallvec::SmallVec::new();
for _ in 0..req_count {
required.push(reader.read_string_id());
}
let mut optional = smallvec::SmallVec::new();
for _ in 0..opt_count {
optional.push(reader.read_string_id());
}
let total = req_count + opt_count;
let mut param_spans = Vec::with_capacity(total);
for _ in 0..total {
let name = reader.read_string_id();
let span_id = reader.read_u32();
param_spans.push((name, span_id));
}
let pattern = Gc::new(
mc,
crate::PatternInfo {
required,
optional,
ellipsis: has_ellipsis,
param_spans: param_spans.into_boxed_slice(),
},
);
let closure = Gc::new(
mc,
crate::Closure {
ip: entry_point,
n_locals,
env: m.env(),
pattern: Some(pattern),
},
);
m.push(Value::new(closure));
Step::Continue(())
}
-358
View File
@@ -1,358 +0,0 @@
use fix_error::Error;
use fix_lang::StringId;
use fix_runtime::{Machine, MachineExt, NixType, resolve_operand};
use gc_arena::{Gc, RefLock};
use smallvec::SmallVec;
use crate::{
AttrSet, BytecodeReader, List, Step, StrictValue, Value, VmRuntimeCtx, VmRuntimeCtxExt,
};
#[inline(always)]
pub(crate) fn op_make_attrs<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &gc_arena::Mutation<'gc>,
) -> Step {
let static_count = reader.read_u32() as usize;
let dynamic_count = reader.read_u32() as usize;
for i in 0..dynamic_count {
let depth = dynamic_count - 1 - i;
m.force_slot_to_pc(depth, reader, mc, reader.inst_start_pc())?;
}
let mut dyn_keys: SmallVec<[_; 2]> = SmallVec::with_capacity(dynamic_count);
for i in 0..dynamic_count {
let depth = dynamic_count - 1 - i;
let key_val = m.peek_forced(depth);
let key_sid = match ctx.get_string_id(key_val) {
Ok(id) => Some(id),
Err(NixType::Null) => None,
Err(got) => return m.finish_type_err(NixType::String, got),
};
dyn_keys.push(key_sid);
}
m.drop_n(dynamic_count);
let mut kv: SmallVec<[(crate::StringId, Value); 4]> =
SmallVec::with_capacity(static_count + dynamic_count);
for _ in 0..static_count {
let key = reader.read_string_id();
let val = resolve_operand(&reader.read_operand_data(), mc, ctx, m);
let _span_id = reader.read_u32();
kv.push((key, val));
}
for key in dyn_keys {
let val = resolve_operand(&reader.read_operand_data(), mc, ctx, m);
let _span_id = reader.read_u32();
if let Some(key) = key {
kv.push((key, val))
}
}
kv.sort_by_key(|(k, _)| *k);
let attrs = Gc::new(mc, AttrSet::from_sorted_unchecked(kv));
m.push(Value::new(attrs));
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_make_empty_attrs<'gc, M: Machine<'gc>>(m: &mut M) -> Step {
m.push(m.empty_attrs());
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_select_static<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &gc_arena::Mutation<'gc>,
) -> Step {
let _span_id = reader.read_u32();
let key = reader.read_string_id();
let attrset = m.force_and_retry::<Gc<AttrSet>>(reader, mc)?;
match attrset.lookup(key) {
Some(v) => {
m.push(v);
}
None => return select_skip(m, key, ctx, reader),
}
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_select_dynamic<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &gc_arena::Mutation<'gc>,
) -> Step {
let _span_id = reader.read_u32();
let (attrset, key_val) = m.force_and_retry::<(Gc<AttrSet>, StrictValue)>(reader, mc)?;
let key_sid = match ctx.get_string_id(key_val) {
Ok(id) => id,
Err(got) => return m.finish_type_err(NixType::String, got),
};
match attrset.lookup(key_sid) {
Some(v) => {
m.push(v);
}
None => return select_skip(m, key_sid, ctx, reader),
}
Step::Continue(())
}
/// Skip the rest of a **Select** attrpath after a missing attribute.
/// Only recognises Select opcodes and jumps; encountering any other
/// opcode means we've reached the end of the select sequence and
/// should report the missing-attribute error.
#[expect(
clippy::cast_sign_loss,
reason = "jump target = pc + offset is a non-negative bytecode address by codegen invariant"
)]
fn select_skip<'gc, M: Machine<'gc>>(
m: &mut M,
key: StringId,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
) -> Step {
use fix_bytecode::Op::*;
loop {
match reader.read_op() {
// Skip rest of the attrpath
SelectStatic => {
reader.set_pc(reader.pc() + 4 + 4);
}
SelectDynamic => {
reader.set_pc(reader.pc() + 4);
}
JumpIfSelectSucceeded => {
reader.set_pc(reader.pc() + 4);
break Step::Continue(());
}
// Default (`a.b or c`)
JumpIfSelectFailed => {
let offset = reader.read_i32();
reader.set_pc(((reader.pc() as isize) + (offset as isize)) as usize);
}
// Report error
_ => {
let name = ctx.resolve_string(key);
return m.finish_err(Error::eval_error(format!("attribute '{name}' missing")));
}
}
}
}
/// Skip the rest of a **HasAttr** attrpath after an intermediate
/// lookup failed. Only recognises HasAttr opcodes and jumps.
#[expect(
clippy::cast_sign_loss,
reason = "jump target = pc + offset is a non-negative bytecode address by codegen invariant"
)]
#[expect(
clippy::unreachable,
reason = "has_attr_skip only runs over a codegen-produced HasAttr attrpath sequence; any other opcode is a codegen bug"
)]
fn has_attr_skip(reader: &mut BytecodeReader<'_>) -> Step {
use fix_bytecode::Op::*;
loop {
match reader.read_op() {
HasAttrPathStatic => {
reader.set_pc(reader.pc() + 4 + 4);
}
HasAttrPathDynamic => {
reader.set_pc(reader.pc() + 4);
}
HasAttrStatic => {
reader.set_pc(reader.pc() + 4);
break Step::Continue(());
}
JumpIfSelectFailed => {
let offset = reader.read_i32();
reader.set_pc(((reader.pc() as isize) + (offset as isize)) as usize);
}
HasAttrDynamic => {
break Step::Continue(());
}
HasAttrResolve => {
reader.set_pc(reader.pc() - 1);
break Step::Continue(());
}
other => {
unreachable!("unexpected opcode {:?} in has_attr_skip", other as u8)
}
}
}
}
#[inline(always)]
pub(crate) fn op_has_attr_path_static<'gc, M: Machine<'gc>>(
m: &mut M,
_ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &gc_arena::Mutation<'gc>,
) -> Step {
let _span_id = reader.read_u32();
let key = reader.read_string_id();
let current = m.force_and_retry::<StrictValue>(reader, mc)?;
match current
.downcast::<AttrSet>()
.and_then(|attrs| attrs.lookup(key))
{
Some(v) => {
m.push(v);
}
None => return has_attr_skip(reader),
}
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_has_attr_path_dynamic<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &gc_arena::Mutation<'gc>,
) -> Step {
let _span_id = reader.read_u32();
let (current, key_val) = m.force_and_retry::<(StrictValue, StrictValue)>(reader, mc)?;
let key_sid = match ctx.get_string_id(key_val) {
Ok(id) => id,
Err(got) => return m.finish_type_err(NixType::String, got),
};
match current
.downcast::<AttrSet>()
.and_then(|attrs| attrs.lookup(key_sid))
{
Some(v) => {
m.push(v);
}
None => return has_attr_skip(reader),
}
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_jump_if_select_failed<'gc, M: Machine<'gc>>(
_m: &mut M,
reader: &mut BytecodeReader<'_>,
) -> Step {
// No-op
let _offset = reader.read_i32();
Step::Continue(())
}
#[inline(always)]
#[expect(
clippy::cast_sign_loss,
reason = "jump target = pc + offset is a non-negative bytecode address by codegen invariant"
)]
pub(crate) fn op_jump_if_select_succeeded<'gc, M: Machine<'gc>>(
_m: &mut M,
reader: &mut BytecodeReader<'_>,
) -> Step {
let offset = reader.read_i32();
reader.set_pc(((reader.pc() as isize) + (offset as isize)) as usize);
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_has_attr_static<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &gc_arena::Mutation<'gc>,
) -> Step {
let key = reader.read_string_id();
let current = m.force_and_retry::<StrictValue>(reader, mc)?;
m.push(Value::new(
current
.downcast::<AttrSet>()
.and_then(|attrs| attrs.lookup(key))
.is_some(),
));
// Skip HasAttrResolve
reader.set_pc(reader.pc() + 1);
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_has_attr_dynamic<'gc, M: MachineExt<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &gc_arena::Mutation<'gc>,
) -> Step {
let (current, dyn_key) = m.force_and_retry::<(StrictValue, StrictValue)>(reader, mc)?;
let key_sid = match ctx.get_string_id(dyn_key) {
Ok(id) => id,
Err(got) => return m.finish_type_err(NixType::String, got),
};
m.push(Value::new(
current
.downcast::<AttrSet>()
.and_then(|attrs| attrs.lookup(key_sid))
.is_some(),
));
// Skip HasAttrResolve
reader.set_pc(reader.pc() + 1);
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_has_attr_resolve<'gc, M: Machine<'gc>>(m: &mut M) -> Step {
// If we reach here, has_attr check has failed, push false (AttrSet is already popped)
m.push(Value::new(false));
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_make_list<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &gc_arena::Mutation<'gc>,
) -> Step {
let count = reader.read_u32() as usize;
let mut items: SmallVec<[Value; 4]> = SmallVec::with_capacity(count);
for _ in 0..count {
items.push(resolve_operand(&reader.read_operand_data(), mc, ctx, m));
}
let list = Gc::new(
mc,
List {
inner: RefLock::new(items),
},
);
m.push(Value::new(list));
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_make_empty_list<'gc, M: Machine<'gc>>(m: &mut M) -> Step {
m.push(m.empty_list());
Step::Continue(())
}
-70
View File
@@ -1,70 +0,0 @@
use fix_error::Error;
use fix_runtime::*;
use gc_arena::Mutation;
use crate::{BytecodeReader, Step, VmRuntimeCtx};
#[inline(always)]
#[expect(
clippy::cast_sign_loss,
reason = "jump target = pc + offset is a non-negative bytecode address by codegen invariant"
)]
pub(crate) fn op_jump_if_false<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &gc_arena::Mutation<'gc>,
) -> Step {
let offset = reader.read_i32();
let cond = m.force_and_retry::<StrictValue>(reader, mc)?;
if cond.downcast::<bool>() == Some(false) {
reader.set_pc(((reader.pc() as isize) + (offset as isize)) as usize);
}
Step::Continue(())
}
#[inline(always)]
#[expect(
clippy::cast_sign_loss,
reason = "jump target = pc + offset is a non-negative bytecode address by codegen invariant"
)]
pub(crate) fn op_jump_if_true<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let offset = reader.read_i32();
let cond = m.force_and_retry::<StrictValue>(reader, mc)?;
if cond.downcast::<bool>() == Some(true) {
reader.set_pc(((reader.pc() as isize) + (offset as isize)) as usize);
}
Step::Continue(())
}
#[inline(always)]
#[expect(
clippy::cast_sign_loss,
reason = "jump target = pc + offset is a non-negative bytecode address by codegen invariant"
)]
pub(crate) fn op_jump<'gc, M: Machine<'gc>>(_m: &mut M, reader: &mut BytecodeReader<'_>) -> Step {
let offset = reader.read_i32();
reader.set_pc(((reader.pc() as isize) + (offset as isize)) as usize);
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_assert<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let raw_id = reader.read_string_id();
let raw = ctx.resolve_string(raw_id);
let _span_id = reader.read_u32();
let assertion = m.force_and_retry::<bool>(reader, mc)?;
if !assertion {
// FIXME: use catchable error
return m.finish_err(Error::eval_error(format!("assertion '{raw}' failed")));
}
Step::Continue(())
}
-63
View File
@@ -1,63 +0,0 @@
use fix_runtime::Machine;
use gc_arena::{Gc, Mutation};
use crate::{BytecodeReader, Step, Value};
#[inline(always)]
pub(crate) fn op_push_smi<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
) -> Step {
let val = reader.read_i32();
m.push(Value::new(val));
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_push_bigint<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let val = reader.read_i64();
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(())
}
-183
View File
@@ -1,183 +0,0 @@
use std::path::PathBuf;
use fix_bytecode::Continuation;
use fix_error::Error;
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};
#[inline(always)]
pub(crate) fn op_load_builtins<'gc, M: Machine<'gc>>(m: &mut M) -> Step {
m.push(m.builtins().into());
Step::Continue(())
}
#[inline(always)]
#[expect(
clippy::panic,
reason = "codegen only emits LoadBuiltin with valid BuiltinId bytes; an unknown id is a codegen/bytecode-corruption bug"
)]
pub(crate) fn op_load_builtin<'gc, M: Machine<'gc>>(
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));
m.push(Value::new(PrimOp {
id,
arity: id.info().arity,
dispatch_ip: Continuation::entry_for_builtin(id).ip(),
}));
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_load_repl_binding<'gc, M: Machine<'gc>>(
_m: &mut M,
reader: &mut BytecodeReader<'_>,
) -> Step {
let _name = reader.read_string_id();
todo!("LoadReplBinding");
}
#[inline(always)]
pub(crate) fn op_load_scoped_binding<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
_mc: &gc_arena::Mutation<'gc>,
) -> Step {
let slot_id = reader.read_u32();
let name = reader.read_string_id();
let scope = m.scope_slot(slot_id);
let Some(attrs) = scope.downcast::<AttrSet>() else {
return m.finish_err(Error::eval_error("internal: scope slot is not an attrset"));
};
match attrs.lookup(name) {
Some(val) => {
m.push(val);
Step::Continue(())
}
None => m.finish_err(Error::eval_error(format!(
"scoped binding '{}' not found",
ctx.resolve_string(name)
))),
}
}
#[inline(always)]
pub(crate) fn op_coerce_to_string<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &gc_arena::Mutation<'gc>,
) -> Step {
let val = m.force_and_retry::<StrictValue>(reader, mc)?;
if val.is::<StringId>() || val.is::<NixString>() {
m.push(val.relax());
} else if let Some(p) = val.downcast::<Path>() {
// Coercing a path to a string yields the canonical path text.
// FIXME: copy to store
m.push(Value::new(p.0));
} else {
todo!("coerce other types to string: {:?}", val.ty());
}
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_concat_strings<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &gc_arena::Mutation<'gc>,
) -> Step {
let count = reader.read_u16() as usize;
let _force_string = reader.read_u8() != 0;
let mut total_len = 0;
let mut has_any_context = false;
for i in 0..count {
let val = m.peek_forced(count - 1 - i);
let s = ctx.get_string(val).expect("coerced");
total_len += s.len();
if !ctx.get_string_context(val).is_empty() {
has_any_context = true;
}
}
let mut result = String::with_capacity(total_len);
let mut merged = StringContext::new();
for i in 0..count {
let val = m.peek_forced(count - 1 - i);
let s = ctx.get_string(val).expect("coerced");
result.push_str(s);
if has_any_context {
let ctx = ctx.get_string_context(val);
if !ctx.is_empty() {
merged = merged.merge(ctx);
}
}
}
m.drop_n(count);
if merged.is_empty() {
let sid = ctx.intern_string(result);
m.push(Value::new(sid));
} else {
let ns = gc_arena::Gc::new(mc, NixString::with_context(result, merged));
m.push(Value::new(ns));
}
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_resolve_path<'gc, M: MachineExt<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &gc_arena::Mutation<'gc>,
) -> Step {
let path_val = m.force_and_retry::<StrictValue>(reader, mc)?;
let dir_id = reader.read_string_id();
// Already a path: keep as-is. ResolvePath is idempotent on paths.
if let Some(p) = path_val.downcast::<Path>() {
m.push(Value::new(p));
return Step::Continue(());
}
let path = match ctx.get_string(path_val) {
Some(s) => s.to_owned(),
None => {
return m.finish_err(Error::eval_error(format!(
"expected a string for path, got {}",
path_val.ty()
)));
}
};
let resolved = match resolve_path_str(ctx.resolve_string(dir_id), &path) {
Ok(s) => s,
Err(e) => return m.finish_err(e),
};
let sid = ctx.intern_string(resolved);
m.push(Value::new(Path(sid)));
Step::Continue(())
}
fn resolve_path_str(current_dir: &str, path: &str) -> Result<String, Box<Error>> {
let raw = if path.starts_with('/') {
return Ok(canon_path_str(path));
} else if let Some(rest) = path.strip_prefix("~/") {
let mut dir =
std::env::home_dir().ok_or_else(|| Error::eval_error("home dir not defined"))?;
dir.push(rest);
dir
} else {
let mut dir = PathBuf::from(current_dir);
dir.push(path);
dir
};
Ok(canon_path_str(&raw))
}
-19
View File
@@ -1,19 +0,0 @@
pub(crate) mod arithmetic;
pub(crate) mod calls;
pub(crate) mod closures;
pub(crate) mod collections;
pub(crate) mod control;
pub(crate) mod literals;
pub(crate) mod misc;
pub(crate) mod variables;
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::*;
-68
View File
@@ -1,68 +0,0 @@
use fix_runtime::Machine;
use crate::{BytecodeReader, Mutation, Step, Value};
#[inline(always)]
#[expect(
clippy::indexing_slicing,
reason = "local slot index is produced by codegen and bounded by the frame's AllocLocals count"
)]
pub(crate) fn op_load_local<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
) -> Step {
let idx = reader.read_u32() as usize;
m.push(m.env().borrow().locals[idx]);
Step::Continue(())
}
#[inline(always)]
#[expect(
clippy::indexing_slicing,
reason = "local slot index is produced by codegen and bounded by the target frame's AllocLocals count"
)]
pub(crate) fn op_load_outer<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
) -> Step {
let layer = reader.read_u8();
let idx = reader.read_u32() as usize;
let mut cur = m.env();
for _ in 0..layer {
let prev = cur.borrow().prev.expect("LoadOuter: env chain too short");
cur = prev;
}
let val = cur.borrow().locals[idx];
m.push(val);
Step::Continue(())
}
#[inline(always)]
#[expect(
clippy::indexing_slicing,
reason = "local slot index is produced by codegen and bounded by the frame's AllocLocals count"
)]
pub(crate) fn op_store_local<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let idx = reader.read_u32() as usize;
let val = m.pop();
m.env().borrow_mut(mc).locals[idx] = val;
Step::Continue(())
}
#[inline(always)]
pub(crate) fn op_alloc_locals<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let count = reader.read_u32() as usize;
m.env()
.borrow_mut(mc)
.locals
.extend(std::iter::repeat_n(Value::default(), count));
Step::Continue(())
}
-84
View File
@@ -1,84 +0,0 @@
use fix_error::Error;
use fix_lang::Symbol;
use fix_runtime::{resolve_operand, *};
use smallvec::SmallVec;
use crate::{Break, BytecodeReader, CallFrame, Step, VmRuntimeCtx};
#[inline(always)]
#[expect(
clippy::indexing_slicing,
clippy::cast_sign_loss,
reason = "counter is a non-negative with-scope index in 0..n, staying within the namespaces vec of length n"
)]
pub(crate) fn op_lookup_with<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &gc_arena::Mutation<'gc>,
) -> Step {
let counter = m
.peek_forced(0)
.downcast::<i32>()
.expect("stack slot must be an integer");
let name = reader.read_string_id();
let n = reader.read_u8();
let mut namespaces = SmallVec::<[_; 2]>::new();
for _ in 0..n {
namespaces.push(resolve_operand(&reader.read_operand_data(), mc, ctx, m));
}
let resume_pc = reader.inst_start_pc();
let namespace = match namespaces[counter as usize].restrict() {
Ok(val) => val,
Err(thunk) => {
let mut state = thunk.borrow_mut(mc);
match *state {
ThunkState::Pending { ip, env } => {
*state = ThunkState::Blackhole;
m.push_call_frame(CallFrame {
thunk: Some(thunk),
pc: resume_pc,
env: m.env(),
depth: None,
});
m.set_env(env);
reader.set_pc(ip);
return Step::Break(Break::Force);
}
ThunkState::Evaluated(v) => v,
ThunkState::Apply { func, arg } => {
m.push_call_frame(CallFrame {
thunk: Some(thunk),
pc: resume_pc,
env: m.env(),
depth: None,
});
m.push(func);
return m.call(reader, mc, arg, resume_pc);
}
ThunkState::Blackhole => {
return m.finish_err(Error::eval_error("infinite recursion encountered"));
}
}
}
};
if let Some(val) = namespace
.downcast::<AttrSet>()
.and_then(|attrs| attrs.lookup(name))
{
m.replace(0, val);
} else if counter + 1 == n as i32 {
return m.finish_err(Error::eval_error(format!(
"undefined variable '{}'",
Symbol::from(ctx.resolve_string(name))
)));
} else {
m.replace(0, Value::new(counter + 1));
reader.set_pc(resume_pc);
}
Step::Continue(())
}
-585
View File
@@ -1,585 +0,0 @@
#![cfg_attr(
feature = "tailcall",
expect(incomplete_features, reason = "for testing purpose only")
)]
#![cfg_attr(
feature = "tailcall",
feature(explicit_tail_calls, rust_preserve_none_cc)
)]
use std::path::PathBuf;
use fix_bytecode::{Continuation, InstructionPtr};
use fix_error::{Error, Result, Source};
use fix_lang::{BuiltinId, StringId};
use gc_arena::metrics::Pacing;
use gc_arena::{Arena, Collect, Gc, Mutation, RefLock, Rootable};
use hashbrown::HashMap;
use smallvec::SmallVec;
#[cfg(feature = "tailcall")]
mod dispatch_tailcall;
pub use fix_runtime::*;
#[doc(hidden)]
#[path = "macro_support.rs"]
pub mod __macro_support;
mod instructions;
mod primops;
extern crate self as fix_vm;
type VmResult<T> = std::result::Result<T, VmError>;
#[derive(Collect)]
#[collect(no_drop)]
pub struct Vm<'gc> {
stack: Vec<Value<'gc>>,
call_stack: Vec<CallFrame<'gc>>,
call_depth: usize,
#[expect(
dead_code,
reason = "error_context is reserved for tryEval catch-frame tracking, not yet wired up"
)]
#[collect(require_static)]
error_context: Vec<ErrorFrame>,
env: GcEnv<'gc>,
import_cache: HashMap<PathBuf, Value<'gc>>,
scope_slots: Vec<Value<'gc>>,
builtins: Gc<'gc, AttrSet<'gc>>,
empty_list: Value<'gc>,
empty_attrs: Value<'gc>,
force_mode: ForceMode,
#[collect(require_static)]
result: Option<Result<fix_lang::Value>>,
#[collect(require_static)]
pending_load: Option<PendingLoad>,
functor_sym: StringId,
}
fn init_builtins<'gc>(mc: &Mutation<'gc>, ctx: &mut impl VmRuntimeCtx) -> Gc<'gc, AttrSet<'gc>> {
let mut entries = SmallVec::with_capacity(BuiltinId::TOTAL);
for id in BuiltinId::ALL {
let arity = id.info().arity;
let name = ctx.intern_string(id.info().name);
let dispatch_ip = Continuation::entry_for_builtin(id).ip();
entries.push((
name,
Value::new(PrimOp {
id,
arity,
dispatch_ip,
}),
));
}
let consts = [
(
"__currentSystem",
Value::new(ctx.intern_string("x86_64-linux")),
),
("__langVersion", Value::new(6i32)),
("__nixVersion", Value::new(ctx.intern_string("2.24.0"))),
("__storeDir", Value::new(ctx.intern_string("/nix/store"))),
("__nixPath", Value::new(Gc::new(mc, List::default()))),
("null", Value::new(Null)),
("true", Value::new(true)),
("false", Value::new(false)),
];
for (name, val) in consts {
let name = name.strip_prefix("__").unwrap_or(name);
let name = ctx.intern_string(name);
entries.push((name, val));
}
let self_ref_thunk = Gc::new(mc, RefLock::new(ThunkState::Blackhole));
let sym = ctx.intern_string("builtins");
entries.push((sym, Value::new(self_ref_thunk)));
entries.sort_by_key(|(k, _)| *k);
let builtins_set = Gc::new(mc, AttrSet::from_sorted_unchecked(entries));
let builtins_value = Value::new(builtins_set);
*self_ref_thunk.borrow_mut(mc) =
ThunkState::Evaluated(builtins_value.restrict().expect("builtins is not a thunk"));
builtins_set
}
impl<'gc> Vm<'gc> {
fn new(force_mode: ForceMode, mc: &Mutation<'gc>, ctx: &mut impl VmRuntimeCtx) -> Self {
let builtins = init_builtins(mc, ctx);
Vm {
stack: Vec::with_capacity(8192),
call_stack: Vec::with_capacity(1024),
call_depth: 0,
error_context: Vec::with_capacity(1024),
env: Gc::new(mc, RefLock::new(Env::empty())),
import_cache: HashMap::new(),
scope_slots: Vec::new(),
builtins,
empty_list: Value::new(Gc::new(mc, List::default())),
empty_attrs: Value::new(Gc::new(mc, AttrSet::default())),
force_mode,
result: None,
pending_load: None,
functor_sym: ctx.intern_string("__functor"),
}
}
}
impl<'gc> Machine<'gc> for Vm<'gc> {
#[inline(always)]
fn push(&mut self, val: Value<'gc>) {
self.stack.push(val);
}
#[inline(always)]
fn pop(&mut self) -> Value<'gc> {
self.stack.pop().expect("stack underflow")
}
#[inline(always)]
fn peek(&self, depth: usize) -> Value<'gc> {
*self
.stack
.get(self.stack.len() - depth - 1)
.expect("stack underflow")
}
#[inline(always)]
fn peek_forced(&self, depth: usize) -> StrictValue<'gc> {
self.stack
.get(self.stack.len() - depth - 1)
.expect("stack underflow")
.restrict()
.expect("forced")
}
#[inline(always)]
fn pop_forced(&mut self) -> StrictValue<'gc> {
self.stack
.pop()
.expect("stack underflow")
.restrict()
.expect("forced")
}
#[inline(always)]
fn replace(&mut self, depth: usize, val: Value<'gc>) {
let len = self.stack.len();
*self
.stack
.get_mut(len - depth - 1)
.expect("stack underflow") = val;
}
#[inline(always)]
fn drop_n(&mut self, depth: usize) {
self.stack.truncate(self.stack.len() - depth);
}
#[inline(always)]
fn stack_len(&self) -> usize {
self.stack.len()
}
#[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).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)]
fn call(
&mut self,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
arg: Value<'gc>,
resume_pc: usize,
) -> Step {
instructions::call(self, reader, mc, arg, resume_pc)
}
#[inline(always)]
fn push_call_frame(&mut self, frame: CallFrame<'gc>) {
self.call_stack.push(frame);
}
#[inline(always)]
fn pop_call_frame(&mut self) -> Option<CallFrame<'gc>> {
self.call_stack.pop()
}
#[inline(always)]
fn call_depth(&self) -> usize {
self.call_depth
}
#[inline(always)]
fn inc_call_depth(&mut self) {
self.call_depth += 1;
}
#[inline(always)]
fn dec_call_depth(&mut self) {
self.call_depth -= 1;
}
#[inline(always)]
fn env(&self) -> GcEnv<'gc> {
self.env
}
#[inline(always)]
fn set_env(&mut self, env: GcEnv<'gc>) {
self.env = env;
}
#[inline(always)]
fn finish_ok(&mut self, val: fix_lang::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 builtins(&self) -> Gc<'gc, AttrSet<'gc>> {
self.builtins
}
#[inline(always)]
fn functor_sym(&self) -> StringId {
self.functor_sym
}
#[inline(always)]
fn empty_list(&self) -> Value<'gc> {
self.empty_list
}
#[inline(always)]
fn empty_attrs(&self) -> Value<'gc> {
self.empty_attrs
}
#[inline(always)]
fn force_mode(&self) -> ForceMode {
self.force_mode
}
#[inline(always)]
fn import_cache_get(&self, path: &std::path::Path) -> Option<Value<'gc>> {
self.import_cache.get(path).copied()
}
#[inline(always)]
fn import_cache_insert(&mut self, path: PathBuf, val: Value<'gc>) {
self.import_cache.insert(path, val);
}
#[inline(always)]
fn scope_slot(&self, idx: u32) -> Value<'gc> {
*self
.scope_slots
.get(idx as usize)
.expect("invalid scope slot")
}
#[inline(always)]
fn scope_slots_push(&mut self, val: Value<'gc>) -> u32 {
let idx = self.scope_slots.len() as u32;
self.scope_slots.push(val);
idx
}
#[inline(always)]
fn set_pending_load(&mut self, load: PendingLoad) {
self.pending_load = Some(load);
}
}
enum Action {
Continue { pc: usize },
Done(Result<fix_lang::Value>),
LoadFile(PendingLoad),
}
/// Compute initial heap size mirroring CppNix's strategy: 25% of physical RAM,
/// clamped to [32 MiB, 384 MiB]. Used as `Pacing::min_sleep` so the collector
/// defers the first cycle until the heap reaches this size.
fn initial_heap_size() -> usize {
const MIN_SIZE: usize = 32 * 1024 * 1024;
const MAX_SIZE: usize = 384 * 1024 * 1024;
let mut sys = sysinfo::System::new();
sys.refresh_memory();
let total = sys.total_memory() as usize;
let quarter = total / 4;
quarter.clamp(MIN_SIZE, MAX_SIZE)
}
impl Vm<'_> {
pub fn run<C: VmContext>(
ctx: &mut C,
ip: InstructionPtr,
force_mode: ForceMode,
) -> Result<fix_lang::Value> {
let (code, runtime) = ctx.split();
let mut arena: Arena<Rootable![Vm<'_>]> = Arena::new(|mc| Vm::new(force_mode, mc, runtime));
arena.metrics().set_pacing(Pacing {
min_sleep: initial_heap_size(),
..Pacing::STOP_THE_WORLD
});
let mut pc = ip.0;
loop {
let bytecode = code.bytecode();
match arena.mutate_root(|mc, root| root.dispatch_batch(bytecode, runtime, pc, mc)) {
Action::Continue { pc: new_pc } => {
pc = new_pc;
if arena.metrics().allocation_debt() > 0.0 {
arena.finish_cycle();
}
}
Action::LoadFile(load) => {
let source = match Source::new_file(load.path) {
Ok(src) => src,
Err(err) => break Err(Error::eval_error(format!("import failed: {err}"))),
};
let extra_scope = load.scope.map(|s| ExtraScope::ScopedImport {
keys: s.keys,
slot_id: s.slot_id,
});
let new_ip = match code.compile_with_scope(source, extra_scope, runtime) {
Ok(ip) => ip,
Err(err) => break Err(err),
};
pc = new_ip.0;
arena.mutate_root(|mc, root| {
root.env = Gc::new(mc, RefLock::new(Env::empty()));
});
}
Action::Done(done) => break done,
}
}
}
}
impl<'gc> Vm<'gc> {
const DEFAULT_FUEL_AMOUNT: u32 = 1024;
#[inline(always)]
fn dispatch_batch<C: VmRuntimeCtx>(
&mut self,
bytecode: &[u8],
ctx: &mut C,
pc: usize,
mc: &Mutation<'gc>,
) -> Action {
#[cfg(not(feature = "tailcall"))]
{
self.execute_batch(bytecode, ctx, pc, mc)
}
#[cfg(feature = "tailcall")]
{
use crate::dispatch_tailcall::{TailResult, run_tailcall};
match run_tailcall(self, mc, ctx, bytecode, pc as u32) {
TailResult::YieldFuel(new_pc) => Action::Continue {
pc: new_pc as usize,
},
TailResult::Done => {
Action::Done(self.result.take().expect("TailResult::Done without result"))
}
TailResult::LoadFile => Action::LoadFile(
self.pending_load
.take()
.expect("TailResult::LoadFile without pending_load"),
),
}
}
}
#[inline(always)]
#[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(
&mut self,
bytecode: &[u8],
ctx: &mut impl VmRuntimeCtx,
pc: usize,
mc: &Mutation<'gc>,
) -> Action {
use fix_bytecode::Op::*;
use instructions::*;
let mut reader = BytecodeReader::new(bytecode, pc);
let mut fuel = Self::DEFAULT_FUEL_AMOUNT;
loop {
use crate::instructions::op_dispatch_cont;
if fuel == 0 {
return Action::Continue { pc: reader.pc() };
}
fuel -= 1;
let op = reader.read_op();
let result = match op {
PushSmi => op_push_smi(self, &mut reader),
PushBigInt => op_push_bigint(self, &mut reader, mc),
PushFloat => op_push_float(self, &mut reader),
PushString => op_push_string(self, &mut reader),
PushNull => op_push_null(self),
PushTrue => op_push_true(self),
PushFalse => op_push_false(self),
LoadLocal => op_load_local(self, &mut reader),
LoadOuter => op_load_outer(self, &mut reader),
StoreLocal => op_store_local(self, &mut reader, mc),
AllocLocals => op_alloc_locals(self, &mut reader, mc),
MakeThunk => op_make_thunk(self, &mut reader, mc),
MakeClosure => op_make_closure(self, &mut reader, mc),
MakePatternClosure => op_make_pattern_closure(self, &mut reader, mc),
Call => op_call(self, ctx, &mut reader, mc),
DispatchCont => op_dispatch_cont(self, ctx, &mut reader, mc),
Return => op_return(self, ctx, &mut reader, mc),
MakeAttrs => op_make_attrs(self, ctx, &mut reader, mc),
MakeEmptyAttrs => op_make_empty_attrs(self),
SelectStatic => op_select_static(self, ctx, &mut reader, mc),
SelectDynamic => op_select_dynamic(self, ctx, &mut reader, mc),
HasAttrPathStatic => op_has_attr_path_static(self, ctx, &mut reader, mc),
HasAttrPathDynamic => op_has_attr_path_dynamic(self, ctx, &mut reader, mc),
HasAttrStatic => op_has_attr_static(self, &mut reader, mc),
HasAttrDynamic => op_has_attr_dynamic(self, ctx, &mut reader, mc),
HasAttrResolve => op_has_attr_resolve(self),
JumpIfSelectFailed => op_jump_if_select_failed(self, &mut reader),
JumpIfSelectSucceeded => op_jump_if_select_succeeded(self, &mut reader),
MakeList => op_make_list(self, ctx, &mut reader, mc),
MakeEmptyList => op_make_empty_list(self),
OpAdd => op_add(self, ctx, &mut reader, mc),
OpSub => op_sub(self, &mut reader, mc),
OpMul => op_mul(self, &mut reader, mc),
OpDiv => op_div(self, &mut reader, mc),
OpEq => op_eq(self, ctx, &mut reader, mc),
OpNeq => op_neq(self, ctx, &mut reader, mc),
OpLt => op_lt(self, ctx, &mut reader, mc),
OpGt => op_gt(self, ctx, &mut reader, mc),
OpLeq => op_leq(self, ctx, &mut reader, mc),
OpGeq => op_geq(self, ctx, &mut reader, mc),
OpConcat => op_concat(self, &mut reader, mc),
OpUpdate => op_update(self, &mut reader, mc),
OpNeg => op_neg(self, &mut reader, mc),
OpNot => op_not(self, &mut reader, mc),
JumpIfFalse => op_jump_if_false(self, &mut reader, mc),
JumpIfTrue => op_jump_if_true(self, &mut reader, mc),
Jump => op_jump(self, &mut reader),
ConcatStrings => op_concat_strings(self, ctx, &mut reader, mc),
CoerceToString => op_coerce_to_string(self, &mut reader, mc),
ResolvePath => op_resolve_path(self, ctx, &mut reader, mc),
Assert => op_assert(self, ctx, &mut reader, mc),
LookupWith => op_lookup_with(self, ctx, &mut reader, mc),
LoadBuiltins => op_load_builtins(self),
LoadBuiltin => op_load_builtin(self, &mut reader),
LoadReplBinding => op_load_repl_binding(self, &mut reader),
LoadScopedBinding => op_load_scoped_binding(self, ctx, &mut reader, mc),
Illegal => unreachable!(),
};
match result {
Step::Continue(()) | Step::Break(Break::Force) => {}
Step::Break(Break::Done) => {
return Action::Done(self.result.take().expect("Break::Done without result"));
}
Step::Break(Break::LoadFile) => {
return Action::LoadFile(
self.pending_load
.take()
.expect("Break::LoadFile without pending_load"),
);
}
}
}
}
}
-36
View File
@@ -1,36 +0,0 @@
//! 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")
}
-499
View File
@@ -1,499 +0,0 @@
//! `builtins.hasContext`, `builtins.getContext`, `builtins.appendContext`,
//! `builtins.unsafeDiscardStringContext`,
//! `builtins.unsafeDiscardOutputDependency`.
//!
//! See `fix-runtime/src/string_context.rs` for the
//! `StringContextElem` type.
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,
StrictValue, StringContext, StringContextElem, Value, VmRuntimeCtx, VmRuntimeCtxExt,
};
use gc_arena::{Gc, Mutation};
use smallvec::SmallVec;
pub fn has_context<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let val = m.force_and_retry::<StrictValue>(reader, mc)?;
if !val.is::<StringId>() && val.downcast::<NixString>().is_none() {
return m.finish_type_err(NixType::String, val.ty());
}
let has_ctx = !ctx.get_string_context(val).is_empty();
m.return_from_primop(Value::new(has_ctx), reader)
}
pub fn unsafe_discard_string_context<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let val = m.force_and_retry::<StrictValue>(reader, mc)?;
if let Some(sid) = val.downcast::<StringId>() {
return m.return_from_primop(Value::new(sid), reader);
}
let Some(ns) = val.downcast::<NixString>() else {
return m.finish_type_err(NixType::String, val.ty());
};
let sid = ctx.intern_string(ns.as_str());
m.return_from_primop(Value::new(sid), reader)
}
pub fn unsafe_discard_output_dependency<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let val = m.force_and_retry::<StrictValue>(reader, mc)?;
if let Some(sid) = val.downcast::<StringId>() {
return m.return_from_primop(Value::new(sid), reader);
}
let Some(ns) = val.downcast::<NixString>() else {
return m.finish_type_err(NixType::String, val.ty());
};
if ns.context().is_empty() {
let sid = ctx.intern_string(ns.as_str());
return m.return_from_primop(Value::new(sid), reader);
}
let mut new_ctx = StringContext::new();
for elem in ns.context() {
let replacement = match elem {
StringContextElem::DrvDeep { drv_path } => StringContextElem::Opaque {
path: drv_path.clone(),
},
other => other.clone(),
};
new_ctx.insert(replacement);
}
let s: Box<str> = ns.as_str().into();
let new_ns = Gc::new(mc, NixString::with_context(s, new_ctx));
m.return_from_primop(Value::new(new_ns), reader)
}
pub fn get_context<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let val = m.force_and_retry::<StrictValue>(reader, mc)?;
if !val.is::<StringId>() && val.downcast::<NixString>().is_none() {
return m.finish_type_err(NixType::String, val.ty());
}
let elems = ctx.get_string_context(val);
struct Info {
path: bool,
all_outputs: bool,
outputs: SmallVec<[Box<str>; 2]>,
}
impl Info {
fn new() -> Self {
Self {
path: false,
all_outputs: false,
outputs: SmallVec::new(),
}
}
}
let mut by_path: std::collections::BTreeMap<Box<str>, Info> = std::collections::BTreeMap::new();
for elem in elems {
match elem {
StringContextElem::Opaque { path } => {
by_path.entry(path.clone()).or_insert_with(Info::new).path = true;
}
StringContextElem::DrvDeep { drv_path } => {
by_path
.entry(drv_path.clone())
.or_insert_with(Info::new)
.all_outputs = true;
}
StringContextElem::Built { drv_path, output } => {
by_path
.entry(drv_path.clone())
.or_insert_with(Info::new)
.outputs
.push(output.clone());
}
}
}
let mut outer_entries: SmallVec<[(StringId, Value<'gc>); 4]> = SmallVec::new();
for (path, mut info) in by_path {
info.outputs.sort();
info.outputs.dedup();
let mut sub: SmallVec<[(StringId, Value<'gc>); 4]> = SmallVec::new();
if info.all_outputs {
sub.push((ctx.intern_string("allOutputs"), Value::new(true)));
}
if !info.outputs.is_empty() {
let items: smallvec::SmallVec<[Value<'gc>; 4]> = info
.outputs
.iter()
.map(|o| Value::new(ctx.intern_string(o)))
.collect();
let list = VmList::new(mc, items);
sub.push((ctx.intern_string("outputs"), Value::new(list)));
}
if info.path {
sub.push((ctx.intern_string("path"), Value::new(true)));
}
sub.sort_by_key(|(k, _)| *k);
let sub_attrs = Gc::new(mc, AttrSet::from_sorted_unchecked(sub));
outer_entries.push((ctx.intern_string(&path), Value::new(sub_attrs)));
}
outer_entries.sort_by_key(|(k, _)| *k);
let outer = Gc::new(mc, AttrSet::from_sorted_unchecked(outer_entries));
m.return_from_primop(Value::new(outer), reader)
}
/// appendContext :: String -> AttrSet -> String
/// The context AttrSet maps store-path strings to `{ path?: Bool, allOutputs?:
/// Bool, outputs?: [String] }`. Each present field contributes one
/// StringContextElem to the result.
///
/// Requires forcing nested attrset values and list elements lazily, so it's
/// structured as a state machine with the following stack layout:
///
/// [strVal, attrs, idx, acc] - outer loop
/// [strVal, attrs, idx, acc, entryAttrs] - after entry forced
/// [strVal, attrs, idx, acc, list] - after `outputs` forced
/// [strVal, attrs, idx, acc, list, oidx] - output-element loop
/// [strVal, attrs, idx, acc, list, oidx, outElem] - after element forced
///
/// `acc` is a sentinel `NixString` whose `data` is empty and whose `context`
/// is the accumulator. The string value itself is preserved in `strVal` and
/// retrieved at finalization.
///
// TODO: handle thunk-valued `path` and `allOutputs` sub-attrs; currently they
// must be already-evaluated booleans.
pub fn append_context<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let (str_val, attrs) = m.force_and_retry::<(StrictValue, Gc<AttrSet>)>(reader, mc)?;
let initial_ctx: StringContext = ctx.get_string_context(str_val).clone();
let acc = Gc::new(mc, NixString::with_context("", initial_ctx));
m.push(str_val.relax());
m.push(Value::new(attrs));
m.push(Value::new(0i32));
m.push(Value::new(acc));
reader.set_pc(Continuation::PAppendContextLoop.ip() as usize);
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>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let idx = m
.peek(1)
.downcast::<i32>()
.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() {
return append_context_finalize(m, ctx, reader, mc);
}
let entry_val = attrs.entries[idx as usize].1;
m.push(entry_val);
m.force_slot_to_pc(
0,
reader,
mc,
Continuation::PAppendContextEntryForced.ip() as usize,
)?;
reader.set_pc(Continuation::PAppendContextEntryForced.ip() as usize);
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>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
// Stack: [strVal, attrs, idx, acc, entryAttrs(thunk)]
// The slot still holds the Thunk pointer; re-force to extract the now-
// Evaluated value into the slot.
m.force_slot(0, reader, mc)?;
let entry_val = m.peek_forced(0);
let Some(entry_attrs) = entry_val.downcast::<AttrSet>() else {
return m.finish_type_err(NixType::AttrSet, entry_val.ty());
};
let idx = m
.peek(2)
.downcast::<i32>()
.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_str_owned: Box<str> = ctx.resolve_string(path_key).into();
if !path_str_owned.starts_with("/nix/store/") {
return m.finish_err(Error::eval_error(format!(
"context key '{path_str_owned}' is not a store path"
)));
}
// Eagerly handle `path` and `allOutputs` (assumed already-forced
// booleans - most callers either set them to literal `true` or omit
// them entirely).
// TODO: force these two attributes correctly
let path_id = ctx.intern_string("path");
let all_outputs_id = ctx.intern_string("allOutputs");
let outputs_id = ctx.intern_string("outputs");
let acc_gc = m
.peek(1)
.downcast::<NixString>()
.expect("stack slot must be a string");
let mut new_acc: StringContext = acc_gc.context().iter().cloned().collect();
if let Some(v) = entry_attrs.lookup(path_id)
&& v.downcast::<bool>() == Some(true)
{
new_acc.insert(StringContextElem::Opaque {
path: path_str_owned.clone(),
});
}
if let Some(v) = entry_attrs.lookup(all_outputs_id)
&& v.downcast::<bool>() == Some(true)
{
if !path_str_owned.ends_with(".drv") {
return m.finish_err(Error::eval_error(format!(
"tried to add all-outputs context of {path_str_owned}, which is not a derivation, to a string"
)));
}
new_acc.insert(StringContextElem::DrvDeep {
drv_path: path_str_owned.clone(),
});
}
let new_acc_gc = Gc::new(mc, NixString::with_context("", new_acc));
m.replace(1, Value::new(new_acc_gc));
if let Some(outputs_val) = entry_attrs.lookup(outputs_id) {
m.replace(0, outputs_val);
m.force_slot_to_pc(
0,
reader,
mc,
Continuation::PAppendContextOutputsForced.ip() as usize,
)?;
reader.set_pc(Continuation::PAppendContextOutputsForced.ip() as usize);
return Step::Continue(());
}
m.drop_n(1);
let idx_back = m
.peek(1)
.downcast::<i32>()
.expect("stack slot must be an integer");
m.replace(1, Value::new(idx_back + 1));
reader.set_pc(Continuation::PAppendContextLoop.ip() as usize);
Step::Continue(())
}
pub fn append_context_outputs_forced<'gc, M: Machine<'gc>>(
m: &mut M,
_ctx: &mut impl VmRuntimeCtx,
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.downcast::<VmList>() else {
return m.finish_type_err(NixType::List, list_val.ty());
};
if list.inner.borrow().is_empty() {
// Stack: [strVal, attrs, idx, acc, list] -> drop list, bump idx.
m.drop_n(1);
let idx_back = m
.peek(1)
.downcast::<i32>()
.expect("stack slot must be an integer");
m.replace(1, Value::new(idx_back + 1));
reader.set_pc(Continuation::PAppendContextLoop.ip() as usize);
return Step::Continue(());
}
m.push(Value::new(0i32));
reader.set_pc(Continuation::PAppendContextOutputElementLoop.ip() as usize);
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>>(
m: &mut M,
_ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let oidx = m
.peek(0)
.downcast::<i32>()
.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();
if oidx as usize >= len {
// Stack: [strVal, attrs, idx, acc, list, oidx] -> drop oidx & list,
// bump idx in place.
m.drop_n(2);
let idx_back = m
.peek(1)
.downcast::<i32>()
.expect("stack slot must be an integer");
m.replace(1, Value::new(idx_back + 1));
reader.set_pc(Continuation::PAppendContextLoop.ip() as usize);
return Step::Continue(());
}
let elem = list.inner.borrow()[oidx as usize];
m.push(elem);
m.force_slot_to_pc(
0,
reader,
mc,
Continuation::PAppendContextOutputElementForced.ip() as usize,
)?;
reader.set_pc(Continuation::PAppendContextOutputElementForced.ip() as usize);
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>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
m.force_slot(0, reader, mc)?;
let elem = m.peek_forced(0);
let Some(output_name) = ctx.get_string(elem) else {
return m.finish_type_err(NixType::String, elem.ty());
};
let output_name: Box<str> = output_name.into();
let idx = m
.peek(4)
.downcast::<i32>()
.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_str: Box<str> = ctx.resolve_string(path_key).into();
if !path_str.ends_with(".drv") {
return m.finish_err(Error::eval_error(format!(
"tried to add derivation output context of {path_str}, which is not a derivation, to a string"
)));
}
let acc_gc = m
.peek(3)
.downcast::<NixString>()
.expect("stack slot must be a string");
let mut new_acc: StringContext = acc_gc.context().iter().cloned().collect();
new_acc.insert(StringContextElem::Built {
drv_path: path_str,
output: output_name,
});
let new_acc_gc = Gc::new(mc, NixString::with_context("", new_acc));
m.replace(3, Value::new(new_acc_gc));
// Stack: [strVal, attrs, idx, acc, list, oidx, outElem] -> drop outElem,
// bump oidx in place.
m.drop_n(1);
let oidx = m
.peek(0)
.downcast::<i32>()
.expect("stack slot must be an integer");
m.replace(0, Value::new(oidx + 1));
reader.set_pc(Continuation::PAppendContextOutputElementLoop.ip() as usize);
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>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
// Stack: [strVal, attrs, idx, acc]
let acc_gc = m
.pop()
.downcast::<NixString>()
.expect("stack slot must be a string");
m.drop_n(2);
let str_val_raw = m.pop();
// The strVal was already forced at entry; restrict() is infallible here.
let str_val = str_val_raw
.restrict()
.unwrap_or_else(|_| panic!("appendContext: strVal unexpectedly a thunk"));
let s_str = ctx.get_string(str_val).unwrap_or("").to_owned();
let context: StringContext = acc_gc.context().iter().cloned().collect();
let result = if context.is_empty() {
let sid = ctx.intern_string(s_str);
Value::new(sid)
} else {
let ns = Gc::new(mc, NixString::with_context(s_str, context));
Value::new(ns)
};
m.return_from_primop(result, reader)
}
-313
View File
@@ -1,313 +0,0 @@
use fix_bytecode::Continuation;
use fix_error::{Error, Result};
use fix_macros::handler;
use fix_runtime::{
AttrSet, BytecodeReader, Closure, Env, List, Machine, MachineExt, Slot, Step, StrictValue,
Value, VmRuntimeCtx, VmRuntimeCtxExt,
};
use gc_arena::{Gc, Mutation, RefLock};
use smallvec::SmallVec;
use crate::primops::stubs::*;
#[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>,
e1: Slot<Value<'gc>>,
e2: Slot<Value<'gc>>,
) -> Result<Value<'gc>> {
let e1: Slot<StrictValue<'gc>> = force(e1).await?;
if collect_children(e1.get()).is_empty() {
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>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
// stack: [msg] - force msg, then abort with it
m.force_slot(0, reader, mc)?;
let msg_val = m.peek_forced(0);
let msg = ctx.get_string(msg_val).unwrap_or("<non-string-value>");
m.finish_err(Error::eval_error(format!(
"evaluation aborted with the following error message: '{msg}'"
)))
}
pub fn force_result_shallow<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
m.force_slot(0, reader, mc)?;
let val = m.peek_forced(0);
let (count, has_children) = if let Some(attrs) = val.downcast::<AttrSet>() {
let len = attrs.entries.len();
(len, len > 0)
} else if let Some(list) = val.downcast::<List>() {
let len = list.inner.borrow().len();
(len, len > 0)
} else {
(0, false)
};
if !has_children {
let val = m.pop();
return m.finish_ok(ctx.convert_value(val));
}
m.push(Value::new(0i32));
m.push(Value::new(count as i32));
reader.set_pc(Continuation::ForceResultShallowPush.ip() as usize);
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>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let idx = m
.peek(1)
.downcast::<i32>()
.expect("stack slot must be an integer");
let len = m
.peek(0)
.downcast::<i32>()
.expect("stack slot must be an integer");
if idx == len {
m.drop_n(2);
let val = m.pop();
return m.finish_ok(ctx.convert_value(val));
}
let val = m.peek_forced(2);
let child = if let Some(attrs) = val.downcast::<AttrSet>() {
attrs.entries.get(idx as usize).map(|&(_, v)| v)
} else if let Some(list) = val.downcast::<List>() {
list.inner.borrow().get(idx as usize).copied()
} else {
None
};
if let Some(child) = child {
m.replace(1, Value::new(idx + 1));
m.push(child);
m.force_slot_to_pc(
0,
reader,
mc,
Continuation::ForceResultShallowLoop.ip() as usize,
)?;
reader.set_pc(Continuation::ForceResultShallowLoop.ip() as usize);
}
Step::Continue(())
}
pub fn force_result_shallow_loop<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
_mc: &Mutation<'gc>,
) -> Step {
m.drop_n(1);
reader.set_pc(Continuation::ForceResultShallowPush.ip() as usize);
Step::Continue(())
}
pub fn force_result_deep_finish<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let val = m.force_and_retry::<StrictValue>(reader, mc)?;
m.finish_ok(ctx.convert_value(val.relax()))
}
fn is_value_in_seen<'gc>(seen: Gc<'gc, List<'gc>>, val: Value<'gc>) -> bool {
if !is_container(val) {
return false;
}
let target = val.to_bits();
for &v in seen.inner.borrow().iter() {
if v.to_bits() == target {
return true;
}
}
false
}
fn add_value_to_seen<'gc>(seen: Gc<'gc, List<'gc>>, mc: &Mutation<'gc>, val: Value<'gc>) {
if is_container(val) {
seen.unlock(mc).borrow_mut().push(val);
}
}
pub fn call_functor_1<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
// Stack invariant on every (re-)entry: [..., orig_arg, self, functor]
// where `functor` is TOS. Retries during force land back here safely.
let functor = m.force_and_retry::<StrictValue>(reader, mc)?;
// Stack now: [..., orig_arg, self]
let self_val = m.pop();
m.push(functor.relax());
// Stack: [..., orig_arg, functor]
// Call 1: functor(self). Resume into CallFunctor2 once it returns.
m.call(
reader,
mc,
self_val,
Continuation::CallFunctor2.ip() as usize,
)
}
pub fn call_functor_2<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
// Stack on entry: [..., orig_arg, intermediate]
// call_stack top: synthetic frame with caller's resume_pc.
let intermediate = m.pop();
let orig_arg = m.pop();
let saved = m.pop_call_frame().expect("functor outer frame missing");
m.set_env(saved.env);
m.push(intermediate);
// Call 2: intermediate(orig_arg). Resume to caller.
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>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let (func, attrset) = m.force_and_retry::<(Gc<Closure>, Gc<AttrSet>)>(reader, mc)?;
let Closure {
ip,
n_locals,
env,
pattern,
} = *func;
let Some(pattern) = pattern else {
unreachable!()
};
// TODO: get function name
// TODO: param spans
if !pattern.ellipsis {
for key in pattern.required.iter().copied() {
if attrset.lookup(key).is_none() {
let name = ctx.resolve_string(key);
return m.finish_err(Error::eval_error(format!(
"function 'anonymous lambda' called without required argument '{name}'"
)));
}
}
for &(key, _) in attrset.entries.iter() {
let is_expected = pattern.required.contains(&key) || pattern.optional.contains(&key);
if !is_expected {
let name = ctx.resolve_string(key);
return m.finish_err(Error::eval_error(format!(
"function 'anonymous lambda' called with unexpected argument '{name}'"
)));
}
}
}
let new_env = Gc::new(
mc,
RefLock::new(Env::with_arg(Value::new(attrset), n_locals, env)),
);
reader.set_pc(ip as usize);
m.set_env(new_env);
Step::Continue(())
}
fn is_container(val: Value<'_>) -> bool {
val.is::<AttrSet>() || val.is::<List<'_>>()
}
-55
View File
@@ -1,55 +0,0 @@
use fix_error::Error;
use fix_lang::StringId;
use fix_runtime::{
BytecodeReader, Machine, MachineExt, NixString, NixType, Path, Step, StrictValue, Value,
VmRuntimeCtx,
};
use gc_arena::Mutation;
pub fn to_string<'gc, M: Machine<'gc>>(
m: &mut M,
_ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let val = m.force_and_retry::<StrictValue>(reader, mc)?;
if val.is::<StringId>() || val.is::<NixString>() {
return m.return_from_primop(val.relax(), reader);
}
if let Some(p) = val.downcast::<Path>() {
return m.return_from_primop(Value::new(p.0), reader);
}
// TODO: derivations / `__toString` / `outPath`,
// numbers, lists.
m.finish_err(Error::eval_error(format!(
"cannot coerce {} to a string",
val.ty()
)))
}
#[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>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let val = m.force_and_retry::<StrictValue>(reader, mc)?;
let name: &str = match val.ty() {
NixType::Int => "int",
NixType::Float => "float",
NixType::Bool => "bool",
NixType::Null => "null",
NixType::String => "string",
NixType::Path => "path",
NixType::AttrSet => "set",
NixType::List => "list",
NixType::Closure | NixType::PrimOp | NixType::PrimOpApp => "lambda",
NixType::Thunk => unreachable!("forced"),
};
let sid = ctx.intern_string(name);
m.return_from_primop(Value::new(sid), reader)
}
-240
View File
@@ -1,240 +0,0 @@
use fix_bytecode::Continuation;
use fix_runtime::{
AttrSet, BytecodeReader, CallFrame, List, Machine, MachineExt, NixNum, Null, Path, Step,
StrictValue, Value, VmRuntimeCtx, VmRuntimeCtxExt,
};
use gc_arena::{Gc, Mutation};
use smallvec::SmallVec;
pub fn start_eq<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
lhs: StrictValue<'gc>,
rhs: StrictValue<'gc>,
negate: bool,
) -> Step {
match shallow_eq(ctx, lhs, rhs) {
ShallowEq::True => {
m.push(Value::new(!negate));
Step::Continue(())
}
ShallowEq::False => {
m.push(Value::new(negate));
Step::Continue(())
}
ShallowEq::RecurseList(la, lb) => {
let lhs_init: SmallVec<[Value<'gc>; 4]> = la.inner.borrow().iter().copied().collect();
let rhs_init: SmallVec<[Value<'gc>; 4]> = lb.inner.borrow().iter().copied().collect();
enter_eq_machine(m, reader, mc, negate, lhs_init, rhs_init)
}
ShallowEq::RecurseAttrs(a, b) => {
let lhs_init: SmallVec<[Value<'gc>; 4]> = a.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)
}
}
}
pub fn eq_step<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let rhs_q = m
.peek(0)
.downcast::<List>()
.expect("eq state corrupted: rhs_queue");
let lhs_q = m
.peek(1)
.downcast::<List>()
.expect("eq state corrupted: lhs_queue");
let result = m
.peek(2)
.downcast::<bool>()
.expect("eq state corrupted: result");
if !result || lhs_q.inner.borrow().is_empty() {
return finalize(m, reader);
}
let lhs = lhs_q
.unlock(mc)
.borrow_mut()
.pop()
.expect("non-empty lhs queue");
let rhs = rhs_q
.unlock(mc)
.borrow_mut()
.pop()
.expect("non-empty rhs queue");
m.push(lhs);
m.push(rhs);
reader.set_pc(Continuation::EqForce.ip() as usize);
Step::Continue(())
}
pub fn eq_force<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let (lhs, rhs) = m.force_and_retry::<(StrictValue, StrictValue)>(reader, mc)?;
apply_pair(m, ctx, mc, lhs, rhs);
reader.set_pc(Continuation::EqStep.ip() as usize);
Step::Continue(())
}
fn finalize<'gc, M: Machine<'gc>>(m: &mut M, reader: &mut BytecodeReader<'_>) -> Step {
m.drop_n(2);
let result = m
.pop()
.downcast::<bool>()
.expect("eq state corrupted: result");
let negate = m
.pop()
.downcast::<bool>()
.expect("eq state corrupted: negate");
m.return_from_primop(Value::new(result ^ negate), reader)
}
fn apply_pair<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &impl VmRuntimeCtx,
mc: &Mutation<'gc>,
lhs: StrictValue<'gc>,
rhs: StrictValue<'gc>,
) {
match shallow_eq(ctx, lhs, rhs) {
ShallowEq::True => {}
ShallowEq::False => {
m.replace(2, Value::new(false));
}
ShallowEq::RecurseList(la, lb) => {
extend_queues(
m,
mc,
la.inner.borrow().iter().copied(),
lb.inner.borrow().iter().copied(),
);
}
ShallowEq::RecurseAttrs(a, b) => {
extend_queues(
m,
mc,
a.entries.iter().map(|&(_, v)| v),
b.entries.iter().map(|&(_, v)| v),
);
}
}
}
fn extend_queues<'gc, M, L, R>(m: &mut M, mc: &Mutation<'gc>, lhs_iter: L, rhs_iter: R)
where
M: Machine<'gc>,
L: IntoIterator<Item = Value<'gc>>,
R: IntoIterator<Item = Value<'gc>>,
{
let rhs_q = m
.peek(0)
.downcast::<List>()
.expect("eq state corrupted: rhs_queue");
let lhs_q = m
.peek(1)
.downcast::<List>()
.expect("eq state corrupted: lhs_queue");
let mut lq = lhs_q.unlock(mc).borrow_mut();
let mut rq = rhs_q.unlock(mc).borrow_mut();
for (x, y) in lhs_iter.into_iter().zip(rhs_iter) {
lq.push(x);
rq.push(y);
}
}
fn enter_eq_machine<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
negate: bool,
lhs_init: SmallVec<[Value<'gc>; 4]>,
rhs_init: SmallVec<[Value<'gc>; 4]>,
) -> Step {
let resume_pc = reader.pc();
m.push_call_frame(CallFrame {
pc: resume_pc,
thunk: None,
env: m.env(),
depth: None,
});
m.inc_call_depth();
m.push(Value::new(negate));
m.push(Value::new(true));
m.push(Value::new(List::new(mc, lhs_init)));
m.push(Value::new(List::new(mc, rhs_init)));
reader.set_pc(Continuation::EqStep.ip() as usize);
Step::Continue(())
}
enum ShallowEq<'gc> {
True,
False,
RecurseList(Gc<'gc, List<'gc>>, Gc<'gc, List<'gc>>),
RecurseAttrs(Gc<'gc, AttrSet<'gc>>, Gc<'gc, AttrSet<'gc>>),
}
fn shallow_eq<'gc>(
ctx: &impl VmRuntimeCtx,
lhs: StrictValue<'gc>,
rhs: StrictValue<'gc>,
) -> ShallowEq<'gc> {
if let (Some(a), Some(b)) = (lhs.downcast_num(), rhs.downcast_num()) {
let eq = match (a, b) {
(NixNum::Int(a), NixNum::Int(b)) => a == b,
(NixNum::Float(a), NixNum::Float(b)) => a == b,
(NixNum::Int(a), NixNum::Float(b)) => a as f64 == b,
(NixNum::Float(a), NixNum::Int(b)) => a == b as f64,
};
return bool_outcome(eq);
}
if let (Some(a), Some(b)) = (lhs.downcast::<bool>(), rhs.downcast::<bool>()) {
return bool_outcome(a == b);
}
if lhs.is::<Null>() && rhs.is::<Null>() {
return ShallowEq::True;
}
if let (Some(a), Some(b)) = (lhs.downcast::<Path>(), rhs.downcast::<Path>()) {
return bool_outcome(a.0 == b.0);
}
if let (Some(a), Some(b)) = (ctx.get_string(lhs), ctx.get_string(rhs)) {
return bool_outcome(a == b);
}
if let (Some(a), Some(b)) = (lhs.downcast::<List>(), rhs.downcast::<List>()) {
if a.inner.borrow().len() != b.inner.borrow().len() {
return ShallowEq::False;
}
return ShallowEq::RecurseList(a, b);
}
if let (Some(a), Some(b)) = (
lhs.downcast::<AttrSet<'gc>>(),
rhs.downcast::<AttrSet<'gc>>(),
) {
let ae = &a.entries;
let be = &b.entries;
if ae.len() != be.len() {
return ShallowEq::False;
}
for (l, r) in ae.iter().zip(be.iter()) {
if l.0 != r.0 {
return ShallowEq::False;
}
}
return ShallowEq::RecurseAttrs(a, b);
}
ShallowEq::False
}
fn bool_outcome<'gc>(b: bool) -> ShallowEq<'gc> {
if b { ShallowEq::True } else { ShallowEq::False }
}
-199
View File
@@ -1,199 +0,0 @@
use std::path::PathBuf;
use fix_bytecode::Continuation;
use fix_error::Error;
use fix_lang::StringId;
use fix_runtime::{
AttrSet, Break, BytecodeReader, CallFrame, Machine, MachineExt, Path, PendingLoad,
PendingScope, Step, StrictValue, Value, VmRuntimeCtx, VmRuntimeCtxExt, canon_path_str,
};
use gc_arena::{Gc, Mutation};
use hashbrown::HashSet;
pub fn import<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
// stack: [path]
let path_val = m.force_and_retry::<StrictValue>(reader, mc)?;
let path_str = match ctx.get_string_or_path(path_val) {
Some(s) => s.to_owned(),
None => {
return m.finish_err(Error::eval_error(format!(
"expected a path or string, got {}",
path_val.ty()
)));
}
};
let abs = match resolve_import_target(&path_str) {
Ok(p) => p,
Err(e) => return m.finish_err(e),
};
if let Some(cached) = m.import_cache_get(&abs) {
return m.return_from_primop(cached, reader);
}
// Stash the resolved path on the stack as a string-id so the
// finalizer can use it as the cache key. The slot we pop here was
// freed by `force_and_retry`, so we simply push.
let path_sid = ctx.intern_string(abs.to_string_lossy());
m.push(Value::new(path_sid));
let env = m.env();
m.push_call_frame(CallFrame {
pc: Continuation::PImportFinalize.ip() as usize,
thunk: None,
env,
depth: None,
});
m.set_pending_load(PendingLoad {
path: abs,
scope: None,
});
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>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
) -> Step {
// stack: [path_sid, return_value]
let val = m.pop();
let path_sid = m
.pop()
.downcast::<StringId>()
.expect("stack slot must be a string");
// The cache key is keyed by the absolute path string we interned in
// `import`. Resolve it back to the host PathBuf.
let path_str = ctx.resolve_string(path_sid).to_owned();
m.import_cache_insert(PathBuf::from(path_str), val);
m.push(val);
let Some(CallFrame {
pc: ret_pc,
thunk: _,
env,
depth: None,
}) = m.pop_call_frame()
else {
unreachable!()
};
reader.set_pc(ret_pc);
// FIXME:
// m.dec_call_depth();
m.set_env(env);
Step::Continue(())
}
pub fn scoped_import<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
// stack: [scope, path]
let (scope_attrs, path_val) = m.force_and_retry::<(Gc<AttrSet>, StrictValue)>(reader, mc)?;
let path_str = match ctx.get_string_or_path(path_val) {
Some(s) => s.to_owned(),
None => {
return m.finish_err(Error::eval_error(format!(
"expected a path or string, got {}",
path_val.ty()
)));
}
};
let abs = match resolve_import_target(&path_str) {
Ok(p) => p,
Err(e) => return m.finish_err(e),
};
let keys: HashSet<StringId> = scope_attrs.entries.iter().map(|&(k, _)| k).collect();
let slot_id = m.scope_slots_push(Value::new(scope_attrs));
let env = m.env();
m.push_call_frame(CallFrame {
pc: Continuation::PScopedImportFinalize.ip() as usize,
thunk: None,
env,
depth: None,
});
m.set_pending_load(PendingLoad {
path: abs,
scope: Some(PendingScope { keys, slot_id }),
});
Step::Break(Break::LoadFile)
}
pub fn scoped_import_finalize<'gc, M: Machine<'gc>>(
m: &mut M,
_ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
_mc: &Mutation<'gc>,
) -> Step {
// stack: [return_value]
// We intentionally do NOT pop the slot from `scope_slots` so that
// closures or thunks created inside the imported file can still
// resolve their scope after `scopedImport` returns.
let val = m.pop();
m.return_from_primop(val, reader)
}
pub fn path_exists<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let path_val = m.force_and_retry::<StrictValue>(reader, mc)?;
// pathExists requires an absolute path. A `Path` value is
// always absolute; a string is accepted only if it starts with `/`.
let (path, is_path_value) = if let Some(p) = path_val.downcast::<Path>() {
(ctx.resolve_string(p.0).to_owned(), true)
} else if let Some(s) = ctx.get_string(path_val) {
(s.to_owned(), false)
} else {
return m.finish_err(Error::eval_error(format!(
"expected a path or string, got {}",
path_val.ty()
)));
};
if !is_path_value && !path.starts_with('/') {
return m.finish_err(Error::eval_error(format!(
"string '{path}' doesn't represent an absolute path"
)));
}
// CppNix collapses consecutive slashes and resolves `.` / `..` lexically
// before checking. Trailing-slash / trailing-dot mean "must be a directory".
let must_be_dir = path.ends_with('/') || path.ends_with("/.");
let canon = canon_path_str(&path);
let p = std::path::Path::new(&canon);
let exists = if must_be_dir {
std::fs::metadata(p).map(|m| m.is_dir()).unwrap_or(false)
} else {
std::fs::symlink_metadata(p).is_ok()
};
m.return_from_primop(Value::new(exists), reader)
}
/// Convert the user-supplied path string into an absolute, dotted-segment
/// resolved `PathBuf` and append `default.nix` if the target is a directory.
fn resolve_import_target(path: &str) -> Result<PathBuf, Box<Error>> {
let mut abs = PathBuf::from(path);
if !abs.is_absolute() {
return Err(Error::eval_error(format!(
"import: expected an absolute path, got '{path}'"
)));
}
if abs.is_dir() {
abs.push("default.nix");
}
Ok(abs)
}
-116
View File
@@ -1,116 +0,0 @@
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
@@ -1,88 +0,0 @@
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:?}"),
}
}
-43
View File
@@ -1,43 +0,0 @@
use fix_error::Error;
use fix_runtime::{
BytecodeReader, Machine, MachineExt, Path, Step, StrictValue, Value, VmRuntimeCtx,
VmRuntimeCtxExt, canon_path_str,
};
use gc_arena::Mutation;
pub fn to_path<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
// coerce to path THEN TO STRING
let val = m.force_and_retry::<StrictValue>(reader, mc)?;
if let Some(Path(s)) = val.downcast::<Path>() {
return m.return_from_primop(Value::new(s), reader);
}
let Some(s) = ctx.get_string(val) else {
return m.finish_err(Error::eval_error(format!(
"cannot coerce {} to a path",
val.ty()
)));
};
if !s.starts_with('/') {
return m.finish_err(Error::eval_error(format!(
"string '{s}' doesn't represent an absolute path"
)));
}
let canon = canon_path_str(s);
let sid = ctx.intern_string(canon);
m.return_from_primop(Value::new(sid), reader)
}
pub fn is_path<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let val = m.force_and_retry::<StrictValue>(reader, mc)?;
let is_path = val.is::<Path>();
m.return_from_primop(Value::new(is_path), reader)
}
-9
View File
@@ -1,9 +0,0 @@
//! 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};
+77 -29
View File
@@ -3,24 +3,23 @@ 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"
tokio = { version = "1.41", features = [
"rt-multi-thread",
"sync",
"net",
"io-util",
] }
nix-compat = { git = "https://git.snix.dev/snix/snix.git", version = "0.1.0", features = [
"wire",
"async",
] }
# REPL # REPL
anyhow = "1.0" anyhow = "1.0"
rustyline = "18.0" rustyline = "17.0"
# CLI # CLI
clap = { version = "4", features = ["derive"] } clap = { version = "4", features = ["derive"] }
@@ -29,28 +28,77 @@ 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"] } derive_more = { version = "2", features = ["full"] }
# Error Reporting
thiserror = "2" thiserror = "2"
miette = { version = "7.4", features = ["fancy"] }
# Data Structure hashbrown = "0.16"
hashbrown = { workspace = true } string-interner = "0.19"
string-interner = { workspace = true } bumpalo = { version = "3.20", features = [
"allocator-api2",
"boxed",
"collections",
] }
ere = { workspace = true } rust-embed = "8.11"
fix-bytecode = { path = "../fix-bytecode" } regex = "1.11"
fix-compiler = { path = "../fix-compiler" }
fix-error = { path = "../fix-error" } nix-nar = "0.3"
fix-lang = { path = "../fix-lang" } sha2 = "0.10"
fix-runtime = { path = "../fix-runtime" } sha1 = "0.10"
fix-vm = { path = "../fix-vm" } md5 = "0.8"
hex = "0.4"
base64 = "0.22"
reqwest = { version = "0.13", features = [
"blocking",
"rustls",
], default-features = false }
tar = "0.4"
flate2 = "1.0"
xz2 = "0.1"
bzip2 = "0.6"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
# spec 1.0.0
toml = "=0.9.9"
dirs = "6.0"
tempfile = "3.24"
rusqlite = { version = "0.38", features = ["bundled"] }
rnix = "0.14"
rowan = "0.16"
ere = "0.2.4"
num_enum = "0.7.5"
tap = "1.0.1"
ghost-cell = "0.2"
colored = "3.1"
sptr = "0.3"
sealed = "0.6"
small-map = "0.1"
smallvec = "1.15"
[dependencies.gc-arena]
git = "https://github.com/kyren/gc-arena"
rev = "75671ae03f53718357b741ed4027560f14e90836"
features = ["allocator-api2", "hashbrown", "smallvec"]
[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"
test-log = { version = "0.2", features = ["trace"] } test-log = { version = "0.2", features = ["trace"] }
[lints] [[bench]]
workspace = true name = "basic_ops"
harness = false
[[bench]]
name = "builtins"
harness = false
[[bench]]
name = "thunk_scope"
harness = false
+9 -7
View File
@@ -1,16 +1,18 @@
#![allow(clippy::allow_attributes_without_reason)] #![allow(dead_code)]
#![allow(dead_code, clippy::unwrap_used, clippy::unwrap_in_result)]
use fix::Evaluator; use fix::error::{Result, Source};
use fix_error::{Result, Source}; use fix::runtime::Runtime;
use fix_lang::Value; use fix::value::Value;
pub fn eval(expr: &str) -> Value { pub fn eval(expr: &str) -> Value {
Evaluator::new() Runtime::new()
.unwrap()
.eval(Source::new_eval(expr.into()).unwrap()) .eval(Source::new_eval(expr.into()).unwrap())
.unwrap() .unwrap()
} }
pub fn eval_result(expr: &str) -> Result<Value> { pub fn eval_result(expr: &str) -> Result<Value> {
Evaluator::new().eval(Source::new_eval(expr.into()).unwrap()) Runtime::new()
.unwrap()
.eval(Source::new_eval(expr.into()).unwrap())
} }
+39 -103
View File
@@ -1,8 +1,3 @@
#![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;
@@ -27,7 +22,7 @@ impl<T: Default + Copy, const N: usize> ArrayExt<N> for [T; N] {
} }
} }
pub trait RawStore: Sized { pub(crate) trait RawStore: Sized {
fn to_val(self, value: &mut Value); fn to_val(self, value: &mut Value);
fn from_val(value: &Value) -> Self; fn from_val(value: &Value) -> Self;
} }
@@ -82,48 +77,41 @@ 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_select! { #[cfg(target_pointer_width = "64")]
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 let val = (unsafe { value.whole_mut() } as *mut [u8; 8]).cast::<P>();
// sound to reinterpret its storage as a `[u8; 8]` that we then treat as `P`
// (a pointer-sized value).
let val = (unsafe { value.whole_mut() } as *mut [u8; 8]).cast::<P>();
let ptr = Strict::map_addr(ptr, |addr| { let ptr = Strict::map_addr(ptr, |addr| {
addr | (usize::from(value.header().into_raw()) << 48) addr | (usize::from(value.header().into_raw()) << 48)
}); });
// SAFETY: `val` points to the `Value`'s 8-byte storage, which is valid and unsafe { val.write(ptr) };
// suitably aligned to hold `P`. }
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_select! { #[cfg(target_pointer_width = "64")]
target_pointer_width = "64" => { {
// SAFETY: `Value` is `#[repr(C, align(8))]` and exactly 8 bytes, so it let val = (unsafe { value.whole() } as *const [u8; 8]).cast::<P>();
// is sound to reinterpret its storage as a `[u8; 8]` cast to `P`; the let ptr = unsafe { val.read() };
// pointer was originally written through this same `P` layout by Strict::map_addr(ptr, |addr| addr & 0x0000_FFFF_FFFF_FFFF)
// `store_ptr`. }
let val = (unsafe { value.whole() } as *const [u8; 8]).cast::<P>();
// SAFETY: `val` points to the `Value`'s 8-byte storage, which is valid #[cfg(target_pointer_width = "32")]
// and suitably aligned to hold `P`. {
let ptr = unsafe { val.read() }; let _ = value;
Strict::map_addr(ptr, |addr| addr & 0x0000_FFFF_FFFF_FFFF) unimplemented!("32-bit pointer storage not supported");
}
_ => {
compile_error!("unsupported pointer width");
}
} }
} }
@@ -167,32 +155,12 @@ enum TagVal {
} }
#[derive(Copy, Clone, PartialEq, Eq)] #[derive(Copy, Clone, PartialEq, Eq)]
pub struct RawTag(TagVal); pub(crate) struct RawTag(TagVal);
impl RawTag { impl RawTag {
pub const P1: RawTag = RawTag(TagVal::_P1);
pub const P2: RawTag = RawTag(TagVal::_P2);
pub const P3: RawTag = RawTag(TagVal::_P3);
pub const P4: RawTag = RawTag(TagVal::_P4);
pub const P5: RawTag = RawTag(TagVal::_P5);
pub const P6: RawTag = RawTag(TagVal::_P6);
pub const P7: RawTag = RawTag(TagVal::_P7);
pub const N1: RawTag = RawTag(TagVal::_N1);
pub const N2: RawTag = RawTag(TagVal::_N2);
pub const N3: RawTag = RawTag(TagVal::_N3);
pub const N4: RawTag = RawTag(TagVal::_N4);
pub const N5: RawTag = RawTag(TagVal::_N5);
pub const N6: RawTag = RawTag(TagVal::_N6);
pub const N7: RawTag = RawTag(TagVal::_N7);
#[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) }
} }
@@ -225,7 +193,7 @@ impl RawTag {
/// `val` must be in the range `1..8` /// `val` must be in the range `1..8`
#[inline] #[inline]
#[must_use] #[must_use]
pub(crate) const unsafe fn new_unchecked(neg: bool, val: u8) -> RawTag { pub(crate) unsafe fn new_unchecked(neg: bool, val: u8) -> RawTag {
RawTag(match (neg, val) { RawTag(match (neg, val) {
(false, 1) => TagVal::_P1, (false, 1) => TagVal::_P1,
(false, 2) => TagVal::_P2, (false, 2) => TagVal::_P2,
@@ -243,9 +211,6 @@ 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() },
}) })
} }
@@ -277,7 +242,7 @@ impl RawTag {
#[inline] #[inline]
#[must_use] #[must_use]
pub const fn neg_val(self) -> (bool, u8) { pub(crate) fn neg_val(self) -> (bool, u8) {
match self.0 { match self.0 {
TagVal::_P1 => (false, 1), TagVal::_P1 => (false, 1),
TagVal::_P2 => (false, 2), TagVal::_P2 => (false, 2),
@@ -318,21 +283,17 @@ impl Header {
} }
#[inline] #[inline]
const fn tag(self) -> RawTag { 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()) }
} }
#[inline] #[inline]
const fn get_sign(self) -> bool { fn get_sign(self) -> bool {
self.0 & 0x8000 != 0 self.0 & 0x8000 != 0
} }
#[inline] #[inline]
const fn get_tag(self) -> u8 { fn get_tag(self) -> u8 {
(self.0 & 0x0007) as u8 (self.0 & 0x0007) as u8
} }
@@ -378,7 +339,7 @@ impl Value {
#[inline] #[inline]
#[must_use] #[must_use]
pub(crate) const fn tag(&self) -> RawTag { pub(crate) fn tag(&self) -> RawTag {
self.header.tag() self.header.tag()
} }
@@ -394,7 +355,7 @@ impl Value {
#[inline] #[inline]
#[must_use] #[must_use]
pub fn data(&self) -> &[u8; 6] { pub(crate) fn data(&self) -> &[u8; 6] {
&self.data &self.data
} }
@@ -408,9 +369,6 @@ 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 }
} }
@@ -418,10 +376,6 @@ 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 }
} }
} }
@@ -461,11 +415,8 @@ impl RawBox {
#[inline] #[inline]
#[must_use] #[must_use]
pub(crate) const fn tag(&self) -> Option<RawTag> { pub(crate) 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
@@ -475,18 +426,12 @@ 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) 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 })
} }
@@ -494,8 +439,6 @@ 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
@@ -506,9 +449,6 @@ 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
@@ -517,16 +457,12 @@ 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 }
} }
} }
+428 -286
View File
@@ -1,26 +1,108 @@
use fix_bytecode::{Const, InstructionPtr, Op, OperandType}; use std::ops::Deref;
use fix_lang::StringId;
use gc_arena::Collect;
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 _;
mod context; use crate::ir::{ArgId, Attr, BinOpKind, Ir, Param, RawIrRef, StringId, ThunkId, UnOpKind};
pub mod ir; use crate::runtime::BUILTINS;
pub use context::{CodeState, ExtraScope}; use crate::runtime::value::{Null, PrimOp, StaticValue};
pub use fix_bytecode::disassembler;
pub use ir::{Attr, BinOpKind, Ir, MaybeThunk, Param, RawIrRef, ThunkId, UnOpKind};
pub trait BytecodeContext { pub struct InstructionPtr(pub(crate) usize);
#[derive(Collect)]
#[collect(require_static)]
pub struct Bytecode {
pub code: Box<[u8]>,
pub current_dir: String,
}
pub(crate) trait BytecodeContext {
fn intern_string(&mut self, s: &str) -> StringId; fn intern_string(&mut self, s: &str) -> StringId;
fn register_span(&mut self, range: TextRange) -> u32; fn register_span(&mut self, range: TextRange) -> u32;
fn get_code(&self) -> &[u8]; fn get_code(&self) -> &[u8];
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: crate::runtime::value::StaticValue) -> u32;
fn current_source_dir(&mut self) -> StringId; }
#[repr(u8)]
#[derive(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,
CallNoSpan,
MakeAttrs,
MakeEmptyAttrs,
Select,
SelectDefault,
HasAttr,
MakeList,
MakeEmptyList,
OpAdd,
OpSub,
OpMul,
OpDiv,
OpEq,
OpNeq,
OpLt,
OpGt,
OpLeq,
OpGeq,
OpConcat,
OpUpdate,
OpNeg,
OpNot,
JumpIfFalse,
JumpIfTrue,
Jump,
ConcatStrings,
ResolvePath,
Assert,
PushWith,
PopWith,
WithLookup,
LoadBuiltins,
LoadBuiltin,
MkPos,
LoadReplBinding,
LoadScopedBinding,
Return,
} }
struct ScopeInfo { struct ScopeInfo {
depth: u8, depth: u16,
arg_id: Option<ArgId>,
thunk_map: HashMap<ThunkId, u32>, thunk_map: HashMap<ThunkId, u32>,
} }
@@ -29,17 +111,22 @@ struct BytecodeEmitter<'a, Ctx: BytecodeContext> {
scope_stack: Vec<ScopeInfo>, scope_stack: Vec<ScopeInfo>,
} }
pub enum InlineOperand { pub(crate) const OPERAND_CONST: u8 = 0;
Const(Const), pub(crate) const OPERAND_LOCAL: u8 = 1;
BigInt(i64), pub(crate) const OPERAND_BUILTINS: u8 = 2;
Local { layer: u8, local: u32 }, pub(crate) const OPERAND_BIGINT: u8 = 3;
BuiltinConst(StringId),
pub(crate) const KEY_STATIC: u8 = 0;
pub(crate) const KEY_DYNAMIC: u8 = 1;
enum InlineOperand {
Const(StaticValue),
Local { layer: u16, local: u32 },
Builtins, Builtins,
ReplBinding(StringId), BigInt(i64),
ScopedImportBinding { id: StringId, slot_id: u32 },
} }
pub fn compile_bytecode(ir: RawIrRef<'_>, ctx: &mut impl BytecodeContext) -> InstructionPtr { pub(crate) fn compile_bytecode(ir: RawIrRef<'_>, ctx: &mut impl BytecodeContext) -> InstructionPtr {
let ip = ctx.get_code().len(); let ip = ctx.get_code().len();
let mut emitter = BytecodeEmitter::new(ctx); let mut emitter = BytecodeEmitter::new(ctx);
emitter.emit_toplevel(ir); emitter.emit_toplevel(ir);
@@ -54,69 +141,57 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
} }
} }
fn inline_maybe_thunk(&self, val: &MaybeThunk) -> InlineOperand { fn classify_value(&mut self, ir: RawIrRef<'_>) -> InlineOperand {
use MaybeThunk::*; match ir.deref() {
match *val { &Ir::Int(x) => {
Int(x) => { if x <= i32::MAX as i64 {
if let Ok(x) = x.try_into() { InlineOperand::Const(StaticValue::new_inline(x as i32))
InlineOperand::Const(Const::Smi(x))
} else { } else {
InlineOperand::BigInt(x) InlineOperand::BigInt(x)
} }
} }
Float(x) => InlineOperand::Const(Const::Float(x)), &Ir::Float(x) => InlineOperand::Const(StaticValue::new_float(x)),
Bool(b) => InlineOperand::Const(Const::Bool(b)), &Ir::Bool(b) => InlineOperand::Const(StaticValue::new_inline(b)),
Null => InlineOperand::Const(Const::Null), Ir::Null => InlineOperand::Const(StaticValue::new_inline(Null)),
Str(id) => InlineOperand::Const(Const::String(id)), Ir::Str(s) => {
Path(id) => InlineOperand::Const(Const::String(id)), let sid = self.ctx.intern_string(s.deref());
Thunk(id) => { InlineOperand::Const(StaticValue::new_inline(sid))
}
&Ir::Thunk(id) => {
let (layer, local) = self.resolve_thunk(id); let (layer, local) = self.resolve_thunk(id);
InlineOperand::Local { layer, local } InlineOperand::Local { layer, local }
} }
Arg { layer } => InlineOperand::Local { layer, local: 0 }, &Ir::Arg(id) => {
Builtin(id) => InlineOperand::Const(Const::PrimOp(id)), let (layer, local) = self.resolve_arg(id);
BuiltinConst(id) => InlineOperand::BuiltinConst(id), InlineOperand::Local { layer, local }
Builtins => InlineOperand::Builtins,
ReplBinding(id) => InlineOperand::ReplBinding(id),
ScopedImportBinding { slot_id, sym: id } => {
InlineOperand::ScopedImportBinding { slot_id, id }
} }
&Ir::Builtin(id) => {
let arity = BUILTINS[id as usize].1;
InlineOperand::Const(StaticValue::new_inline(PrimOp { id, arity }))
}
Ir::Builtins => InlineOperand::Builtins,
_ => panic!("cannot classify IR node as inline operand"),
} }
} }
fn emit_maybe_thunk(&mut self, val: &MaybeThunk) { fn emit_inline_operand(&mut self, operand: InlineOperand) {
use InlineOperand::*;
let operand = self.inline_maybe_thunk(val);
match operand { match operand {
Const(val) => { InlineOperand::Const(val) => {
let idx = self.ctx.add_constant(val); let idx = self.ctx.add_constant(val);
self.emit_u8(OperandType::Const as u8); self.emit_u8(OPERAND_CONST);
self.emit_u32(idx); self.emit_u32(idx);
} }
BigInt(val) => { InlineOperand::Local { layer, local } => {
self.emit_u8(OperandType::BigInt as u8); self.emit_u8(OPERAND_LOCAL);
self.emit_i64(val); self.emit_u8(layer as u8);
}
Local { layer, local } => {
self.emit_u8(OperandType::Local as u8);
self.emit_u8(layer);
self.emit_u32(local); self.emit_u32(local);
} }
BuiltinConst(id) => { InlineOperand::Builtins => {
self.emit_u8(OperandType::BuiltinConst as u8); self.emit_u8(OPERAND_BUILTINS);
self.emit_str_id(id);
} }
Builtins => { InlineOperand::BigInt(val) => {
self.emit_u8(OperandType::Builtins as u8); self.emit_u8(OPERAND_BIGINT);
} self.emit_i64(val);
ReplBinding(id) => {
self.emit_u8(OperandType::ReplBinding as u8);
self.emit_str_id(id);
}
ScopedImportBinding { id, slot_id } => {
self.emit_u8(OperandType::ScopedImportBinding as u8);
self.emit_u32(slot_id);
self.emit_str_id(id);
} }
} }
} }
@@ -126,11 +201,6 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
self.ctx.get_code_mut().push(op as u8); self.ctx.get_code_mut().push(op as u8);
} }
#[inline]
fn emit_bool(&mut self, val: bool) {
self.emit_u8(u8::from(val));
}
#[inline] #[inline]
fn emit_u8(&mut self, val: u8) { fn emit_u8(&mut self, val: u8) {
self.ctx.get_code_mut().push(val); self.ctx.get_code_mut().push(val);
@@ -178,10 +248,6 @@ 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());
} }
@@ -206,15 +272,11 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
.extend_from_slice(&(id.0.to_usize() as u32).to_le_bytes()); .extend_from_slice(&(id.0.to_usize() as u32).to_le_bytes());
} }
fn current_depth(&self) -> u8 { fn current_depth(&self) -> u16 {
self.scope_stack.last().map_or(0, |s| s.depth) self.scope_stack.last().map_or(0, |s| s.depth)
} }
#[expect( fn resolve_thunk(&self, id: ThunkId) -> (u16, u32) {
clippy::panic,
reason = "a ThunkId must resolve in some enclosing scope; failure indicates a compiler bug"
)]
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) {
let layer = self.current_depth() - scope.depth; let layer = self.current_depth() - scope.depth;
@@ -224,26 +286,166 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
panic!("ThunkId {:?} not found in any scope", id); panic!("ThunkId {:?} not found in any scope", id);
} }
fn emit_load(&mut self, layer: u8, local: u32) { fn resolve_arg(&self, id: ArgId) -> (u16, u32) {
for scope in self.scope_stack.iter().rev() {
if scope.arg_id == Some(id) {
let layer = self.current_depth() - scope.depth;
return (layer, 0);
}
}
panic!("ArgId {:?} not found in any scope", id);
}
fn emit_load(&mut self, layer: u16, local: u32) {
if layer == 0 { if layer == 0 {
self.emit_op(Op::LoadLocal); self.emit_op(Op::LoadLocal);
self.emit_u32(local); self.emit_u32(local);
} else { } else {
self.emit_op(Op::LoadOuter); self.emit_op(Op::LoadOuter);
self.emit_u8(layer); self.emit_u8(layer as u8);
self.emit_u32(local); self.emit_u32(local);
} }
} }
fn push_scope(&mut self, has_arg: bool, thunk_ids: &[ThunkId]) { fn count_with_thunks(&self, ir: RawIrRef<'_>) -> usize {
let depth = self.scope_stack.len().try_into().expect("scope too deep!"); match ir.deref() {
Ir::With { thunks, body, .. } => thunks.len() + self.count_with_thunks(*body),
Ir::TopLevel { thunks, body } => thunks.len() + self.count_with_thunks(*body),
Ir::If { cond, consq, alter } => {
self.count_with_thunks(*cond)
+ self.count_with_thunks(*consq)
+ self.count_with_thunks(*alter)
}
Ir::BinOp { lhs, rhs, .. } => {
self.count_with_thunks(*lhs) + self.count_with_thunks(*rhs)
}
Ir::UnOp { rhs, .. } => self.count_with_thunks(*rhs),
Ir::Call { func, arg, .. } => {
self.count_with_thunks(*func) + self.count_with_thunks(*arg)
}
Ir::Assert {
assertion, expr, ..
} => self.count_with_thunks(*assertion) + self.count_with_thunks(*expr),
Ir::Select { expr, default, .. } => {
self.count_with_thunks(*expr) + default.map_or(0, |d| self.count_with_thunks(d))
}
Ir::HasAttr { lhs, .. } => self.count_with_thunks(*lhs),
Ir::ConcatStrings { parts, .. } => {
parts.iter().map(|p| self.count_with_thunks(*p)).sum()
}
Ir::Path(p) => self.count_with_thunks(*p),
Ir::List { items } => items.iter().map(|item| self.count_with_thunks(*item)).sum(),
Ir::AttrSet { stcs, dyns } => {
stcs.iter()
.map(|(_, &(val, _))| self.count_with_thunks(val))
.sum::<usize>()
+ dyns
.iter()
.map(|&(k, v, _)| self.count_with_thunks(k) + self.count_with_thunks(v))
.sum::<usize>()
}
_ => 0,
}
}
fn collect_all_thunks<'ir>(
&self,
own_thunks: &[(ThunkId, RawIrRef<'ir>)],
body: RawIrRef<'ir>,
) -> Vec<(ThunkId, RawIrRef<'ir>)> {
let mut all = Vec::from(own_thunks);
self.collect_with_thunks_recursive(body, &mut all);
let mut i = 0;
while i < all.len() {
let thunk_body = all[i].1;
self.collect_with_thunks_recursive(thunk_body, &mut all);
i += 1;
}
all
}
fn collect_with_thunks_recursive<'ir>(
&self,
ir: RawIrRef<'ir>,
out: &mut Vec<(ThunkId, RawIrRef<'ir>)>,
) {
match ir.deref() {
Ir::With { thunks, body, .. } => {
for &(id, inner) in thunks.iter() {
out.push((id, inner));
}
self.collect_with_thunks_recursive(*body, out);
}
Ir::TopLevel { thunks, body } => {
for &(id, inner) in thunks.iter() {
out.push((id, inner));
}
self.collect_with_thunks_recursive(*body, out);
}
Ir::If { cond, consq, alter } => {
self.collect_with_thunks_recursive(*cond, out);
self.collect_with_thunks_recursive(*consq, out);
self.collect_with_thunks_recursive(*alter, out);
}
Ir::BinOp { lhs, rhs, .. } => {
self.collect_with_thunks_recursive(*lhs, out);
self.collect_with_thunks_recursive(*rhs, out);
}
Ir::UnOp { rhs, .. } => self.collect_with_thunks_recursive(*rhs, out),
Ir::Call { func, arg, .. } => {
self.collect_with_thunks_recursive(*func, out);
self.collect_with_thunks_recursive(*arg, out);
}
Ir::Assert {
assertion, expr, ..
} => {
self.collect_with_thunks_recursive(*assertion, out);
self.collect_with_thunks_recursive(*expr, out);
}
Ir::Select { expr, default, .. } => {
self.collect_with_thunks_recursive(*expr, out);
if let Some(d) = default {
self.collect_with_thunks_recursive(*d, out);
}
}
Ir::HasAttr { lhs, .. } => self.collect_with_thunks_recursive(*lhs, out),
Ir::ConcatStrings { parts, .. } => {
for p in parts.iter() {
self.collect_with_thunks_recursive(*p, out);
}
}
Ir::Path(p) => self.collect_with_thunks_recursive(*p, out),
Ir::List { items } => {
for item in items.iter() {
self.collect_with_thunks_recursive(*item, out);
}
}
Ir::AttrSet { stcs, dyns } => {
for (_, &(val, _)) in stcs.iter() {
self.collect_with_thunks_recursive(val, out);
}
for &(key, val, _) in dyns.iter() {
self.collect_with_thunks_recursive(key, out);
self.collect_with_thunks_recursive(val, out);
}
}
_ => {}
}
}
fn push_scope(&mut self, has_arg: bool, arg_id: Option<ArgId>, thunk_ids: &[ThunkId]) {
let depth = self.scope_stack.len() as u16;
let thunk_base = if has_arg { 1u32 } else { 0u32 }; let thunk_base = if has_arg { 1u32 } else { 0u32 };
let thunk_map = thunk_ids let thunk_map = thunk_ids
.iter() .iter()
.enumerate() .enumerate()
.map(|(i, &id)| (id, thunk_base + i as u32)) .map(|(i, &id)| (id, thunk_base + i as u32))
.collect(); .collect();
self.scope_stack.push(ScopeInfo { depth, thunk_map }); self.scope_stack.push(ScopeInfo {
depth,
arg_id,
thunk_map,
});
} }
fn pop_scope(&mut self) { fn pop_scope(&mut self) {
@@ -251,13 +453,19 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
} }
fn emit_toplevel(&mut self, ir: RawIrRef<'_>) { fn emit_toplevel(&mut self, ir: RawIrRef<'_>) {
match ir { match ir.deref() {
&Ir::TopLevel { body, ref thunks } => { &Ir::TopLevel { body, ref thunks } => {
let thunk_ids: Vec<ThunkId> = thunks.iter().map(|&(id, _)| id).collect(); let with_thunk_count = self.count_with_thunks(body);
self.push_scope(false, &thunk_ids); let total_slots = thunks.len() + with_thunk_count;
if !thunks.is_empty() {
let all_thunks = self.collect_all_thunks(thunks, body);
let thunk_ids: Vec<ThunkId> = all_thunks.iter().map(|&(id, _)| id).collect();
self.push_scope(false, None, &thunk_ids);
if total_slots > 0 {
self.emit_op(Op::AllocLocals); self.emit_op(Op::AllocLocals);
self.emit_u32(thunks.len().try_into().expect("too many thunks")); self.emit_u32(total_slots as u32);
} }
self.emit_scope_thunks(thunks); self.emit_scope_thunks(thunks);
@@ -267,7 +475,7 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
self.pop_scope(); self.pop_scope();
} }
_ => { _ => {
self.push_scope(false, &[]); self.push_scope(false, None, &[]);
self.emit_expr(ir); self.emit_expr(ir);
self.emit_op(Op::Return); self.emit_op(Op::Return);
self.pop_scope(); self.pop_scope();
@@ -277,6 +485,9 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
fn emit_scope_thunks(&mut self, thunks: &[(ThunkId, RawIrRef<'_>)]) { fn emit_scope_thunks(&mut self, thunks: &[(ThunkId, RawIrRef<'_>)]) {
for &(id, inner) in thunks { for &(id, inner) in thunks {
let label = format!("e{}", id.0);
let label_idx = self.ctx.intern_string(&label);
let skip_patch = self.emit_jump_placeholder(); let skip_patch = self.emit_jump_placeholder();
let entry_point = self.ctx.get_code_mut().len() as u32; let entry_point = self.ctx.get_code_mut().len() as u32;
self.emit_expr(inner); self.emit_expr(inner);
@@ -284,6 +495,7 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
self.patch_jump_target(skip_patch); self.patch_jump_target(skip_patch);
self.emit_op(Op::MakeThunk); self.emit_op(Op::MakeThunk);
self.emit_u32(entry_point); self.emit_u32(entry_point);
self.emit_str_id(label_idx);
let (_, local_idx) = self.resolve_thunk(id); let (_, local_idx) = self.resolve_thunk(id);
self.emit_op(Op::StoreLocal); self.emit_op(Op::StoreLocal);
self.emit_u32(local_idx); self.emit_u32(local_idx);
@@ -291,11 +503,11 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
} }
fn emit_expr(&mut self, ir: RawIrRef<'_>) { fn emit_expr(&mut self, ir: RawIrRef<'_>) {
match ir { match ir.deref() {
&Ir::Int(x) => { &Ir::Int(x) => {
if let Ok(x) = x.try_into() { if x <= i32::MAX as i64 {
self.emit_op(Op::PushSmi); self.emit_op(Op::PushSmi);
self.emit_i32(x); self.emit_i32(x as i32);
} else { } else {
self.emit_op(Op::PushBigInt); self.emit_op(Op::PushBigInt);
self.emit_i64(x); self.emit_i64(x);
@@ -308,15 +520,14 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
&Ir::Bool(true) => self.emit_op(Op::PushTrue), &Ir::Bool(true) => self.emit_op(Op::PushTrue),
&Ir::Bool(false) => self.emit_op(Op::PushFalse), &Ir::Bool(false) => self.emit_op(Op::PushFalse),
Ir::Null => self.emit_op(Op::PushNull), Ir::Null => self.emit_op(Op::PushNull),
&Ir::Str(id) => { Ir::Str(s) => {
let idx = self.ctx.intern_string(s.deref());
self.emit_op(Op::PushString); self.emit_op(Op::PushString);
self.emit_str_id(id); self.emit_str_id(idx);
} }
&Ir::Path(p) => { &Ir::Path(p) => {
self.emit_expr(p); self.emit_expr(p);
self.emit_op(Op::ResolvePath); self.emit_op(Op::ResolvePath);
let dir_id = self.ctx.current_source_dir();
self.emit_str_id(dir_id);
} }
&Ir::If { cond, consq, alter } => { &Ir::If { cond, consq, alter } => {
self.emit_expr(cond); self.emit_expr(cond);
@@ -355,9 +566,10 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
&Ir::Func { &Ir::Func {
body, body,
ref param, ref param,
arg,
ref thunks, ref thunks,
} => { } => {
self.emit_func(thunks, param, body); self.emit_func(arg, thunks, param, body);
} }
Ir::AttrSet { stcs, dyns } => { Ir::AttrSet { stcs, dyns } => {
self.emit_attrset(stcs, dyns); self.emit_attrset(stcs, dyns);
@@ -369,17 +581,21 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
self.emit_op(Op::MakeList); self.emit_op(Op::MakeList);
self.emit_u32(items.len() as u32); self.emit_u32(items.len() as u32);
for &item in items.iter() { for &item in items.iter() {
self.emit_maybe_thunk(item); let operand = self.classify_value(item);
self.emit_inline_operand(operand);
} }
} }
} }
&Ir::Call { func, arg, .. } => { &Ir::Call { func, arg, span } => {
self.emit_expr(arg);
self.emit_expr(func); self.emit_expr(func);
let span_id = self.ctx.register_span(span);
self.emit_op(Op::Call); self.emit_op(Op::Call);
self.emit_maybe_thunk(arg); self.emit_u32(span_id);
} }
&Ir::Arg { layer } => { &Ir::Arg(id) => {
self.emit_load(layer, 0); let (layer, local) = self.resolve_arg(id);
self.emit_load(layer, local);
} }
&Ir::TopLevel { body, ref thunks } => { &Ir::TopLevel { body, ref thunks } => {
self.emit_toplevel_inner(body, thunks); self.emit_toplevel_inner(body, thunks);
@@ -392,6 +608,10 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
} => { } => {
self.emit_select(expr, attrpath, default, span); self.emit_select(expr, attrpath, default, span);
} }
&Ir::Thunk(id) => {
let (layer, local) = self.resolve_thunk(id);
self.emit_load(layer, local);
}
Ir::Builtins => { Ir::Builtins => {
self.emit_op(Op::LoadBuiltins); self.emit_op(Op::LoadBuiltins);
} }
@@ -401,7 +621,7 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
} }
&Ir::BuiltinConst(id) => { &Ir::BuiltinConst(id) => {
self.emit_select( self.emit_select(
&Ir::Builtins, RawIrRef(&Ir::Builtins),
&[Attr::Str(id, TextRange::default())], &[Attr::Str(id, TextRange::default())],
None, None,
TextRange::default(), TextRange::default(),
@@ -411,13 +631,13 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
ref parts, ref parts,
force_string, force_string,
} => { } => {
for &part in parts.iter() {
self.emit_expr(part);
self.emit_op(Op::CoerceToString);
}
self.emit_op(Op::ConcatStrings); self.emit_op(Op::ConcatStrings);
self.emit_u16(parts.len() as u16); self.emit_u16(parts.len() as u16);
self.emit_bool(force_string); self.emit_u8(if force_string { 1 } else { 0 });
for &part in parts.iter() {
let operand = self.classify_value(part);
self.emit_inline_operand(operand);
}
} }
&Ir::HasAttr { lhs, ref rhs } => { &Ir::HasAttr { lhs, ref rhs } => {
self.emit_has_attr(lhs, rhs); self.emit_has_attr(lhs, rhs);
@@ -431,99 +651,38 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
let raw_idx = self.ctx.intern_string(assertion_raw); let raw_idx = self.ctx.intern_string(assertion_raw);
let span_id = self.ctx.register_span(*span); let span_id = self.ctx.register_span(*span);
self.emit_expr(*assertion); self.emit_expr(*assertion);
self.emit_expr(*expr);
self.emit_op(Op::Assert); self.emit_op(Op::Assert);
self.emit_str_id(raw_idx); self.emit_str_id(raw_idx);
self.emit_u32(span_id); self.emit_u32(span_id);
self.emit_expr(*expr); }
&Ir::CurPos(span) => {
let span_id = self.ctx.register_span(span);
self.emit_op(Op::MkPos);
self.emit_u32(span_id);
} }
&Ir::ReplBinding(name) => { &Ir::ReplBinding(name) => {
self.emit_op(Op::LoadReplBinding); self.emit_op(Op::LoadReplBinding);
self.emit_str_id(name); self.emit_str_id(name);
} }
&Ir::ScopedImportBinding { sym, slot_id } => { &Ir::ScopedImportBinding(name) => {
self.emit_op(Op::LoadScopedBinding); self.emit_op(Op::LoadScopedBinding);
self.emit_u32(slot_id); self.emit_str_id(name);
self.emit_str_id(sym);
} }
Ir::WithLookup { sym, namespaces } => { &Ir::With {
// counter namespace,
self.emit_expr(&Ir::Int(0)); body,
self.emit_op(Op::LookupWith); ref thunks,
self.emit_str_id(*sym); } => {
self.emit_u8( self.emit_with(namespace, body, thunks);
namespaces
.len()
.try_into()
.expect("too many `with` namespaces"),
);
for namespace in namespaces {
self.emit_maybe_thunk(namespace);
}
} }
&Ir::MaybeThunk(thunk) => { &Ir::WithLookup(name) => {
use MaybeThunk::*; self.emit_op(Op::WithLookup);
match *thunk { self.emit_str_id(name);
Int(x) => {
if let Ok(x) = x.try_into() {
self.emit_op(Op::PushSmi);
self.emit_i32(x);
} else {
self.emit_op(Op::PushBigInt);
self.emit_i64(x);
}
}
Float(x) => {
self.emit_op(Op::PushFloat);
self.emit_f64(x);
}
Bool(true) => self.emit_op(Op::PushTrue),
Bool(false) => self.emit_op(Op::PushFalse),
Null => self.emit_op(Op::PushNull),
Str(id) => {
self.emit_op(Op::PushString);
self.emit_str_id(id);
}
Path(id) => {
self.emit_op(Op::PushString);
self.emit_str_id(id);
self.emit_op(Op::ResolvePath);
let dir_id = self.ctx.current_source_dir();
self.emit_str_id(dir_id);
}
Thunk(id) => {
let (layer, local) = self.resolve_thunk(id);
self.emit_load(layer, local);
}
Arg { layer } => self.emit_load(layer, 0),
Builtin(id) => {
self.emit_op(Op::LoadBuiltin);
self.emit_u8(id as u8);
}
BuiltinConst(id) => self.emit_select(
&Ir::Builtins,
&[Attr::Str(id, TextRange::default())],
None,
TextRange::default(),
),
Builtins => self.emit_op(Op::LoadBuiltins),
ReplBinding(name) => {
self.emit_op(Op::LoadReplBinding);
self.emit_str_id(name);
}
ScopedImportBinding { slot_id, sym } => {
self.emit_op(Op::LoadScopedBinding);
self.emit_u32(slot_id);
self.emit_str_id(sym);
}
}
} }
} }
} }
#[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 {
@@ -584,6 +743,16 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
let end_offset = (self.ctx.get_code_mut().len() as i32) - (after_jump as i32); let end_offset = (self.ctx.get_code_mut().len() as i32) - (after_jump as i32);
self.patch_i32(end_placeholder, end_offset); self.patch_i32(end_placeholder, end_offset);
} }
PipeL => {
self.emit_expr(rhs);
self.emit_expr(lhs);
self.emit_op(Op::CallNoSpan);
}
PipeR => {
self.emit_expr(lhs);
self.emit_expr(rhs);
self.emit_op(Op::CallNoSpan);
}
_ => { _ => {
self.emit_expr(lhs); self.emit_expr(lhs);
self.emit_expr(rhs); self.emit_expr(rhs);
@@ -606,17 +775,22 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
} }
} }
fn emit_func<'ir>( fn emit_func(
&mut self, &mut self,
thunks: &[(ThunkId, RawIrRef<'ir>)], arg: ArgId,
param: &Option<Param<'ir>>, thunks: &[(ThunkId, RawIrRef<'_>)],
body: RawIrRef<'ir>, param: &Option<Param<'_>>,
body: RawIrRef<'_>,
) { ) {
let thunk_ids: Vec<ThunkId> = thunks.iter().map(|&(id, _)| id).collect(); let with_thunk_count = self.count_with_thunks(body);
let total_slots = thunks.len() + with_thunk_count;
let all_thunks = self.collect_all_thunks(thunks, body);
let thunk_ids: Vec<ThunkId> = all_thunks.iter().map(|&(id, _)| id).collect();
let skip_patch = self.emit_jump_placeholder(); let skip_patch = self.emit_jump_placeholder();
let entry_point = self.ctx.get_code().len() as u32; let entry_point = self.ctx.get_code().len() as u32;
self.push_scope(true, &thunk_ids); self.push_scope(true, Some(arg), &thunk_ids);
self.emit_scope_thunks(thunks); self.emit_scope_thunks(thunks);
self.emit_expr(body); self.emit_expr(body);
self.emit_op(Op::Return); self.emit_op(Op::Return);
@@ -631,10 +805,10 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
{ {
self.emit_op(Op::MakePatternClosure); self.emit_op(Op::MakePatternClosure);
self.emit_u32(entry_point); self.emit_u32(entry_point);
self.emit_u32(thunks.len().try_into().expect("too many thunks")); self.emit_u32(total_slots as u32);
self.emit_u16(required.len() as u16); self.emit_u16(required.len() as u16);
self.emit_u16(optional.len() as u16); self.emit_u16(optional.len() as u16);
self.emit_bool(*ellipsis); self.emit_u8(if *ellipsis { 1 } else { 0 });
for &(sym, _) in required.iter() { for &(sym, _) in required.iter() {
self.emit_str_id(sym); self.emit_str_id(sym);
@@ -650,37 +824,38 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
} else { } else {
self.emit_op(Op::MakeClosure); self.emit_op(Op::MakeClosure);
self.emit_u32(entry_point); self.emit_u32(entry_point);
self.emit_u32(thunks.len().try_into().expect("too many thunks")); self.emit_u32(total_slots as u32);
} }
} }
fn emit_attrset( fn emit_attrset(
&mut self, &mut self,
stcs: &ir::HashMap<'_, StringId, (&MaybeThunk, TextRange)>, stcs: &crate::ir::HashMap<'_, StringId, (RawIrRef<'_>, TextRange)>,
dyns: &[(RawIrRef<'_>, &MaybeThunk, TextRange)], dyns: &[(RawIrRef<'_>, RawIrRef<'_>, TextRange)],
) { ) {
if stcs.is_empty() && dyns.is_empty() { if stcs.is_empty() && dyns.is_empty() {
self.emit_op(Op::MakeEmptyAttrs); self.emit_op(Op::MakeEmptyAttrs);
return; return;
} }
for &(key_expr, _val, _span) in dyns.iter() { let total = stcs.len() + dyns.len();
self.emit_expr(key_expr);
}
self.emit_op(Op::MakeAttrs); self.emit_op(Op::MakeAttrs);
self.emit_u32(stcs.len() as u32); self.emit_u32(total as u32);
self.emit_u32(dyns.len() as u32);
for (&sym, &(val, span)) in stcs.iter() { for (&sym, &(val, span)) in stcs.iter() {
self.emit_u8(KEY_STATIC);
self.emit_str_id(sym); self.emit_str_id(sym);
self.emit_maybe_thunk(val); let val_operand = self.classify_value(val);
self.emit_inline_operand(val_operand);
let span_id = self.ctx.register_span(span); let span_id = self.ctx.register_span(span);
self.emit_u32(span_id); self.emit_u32(span_id);
} }
for &(key, val, span) in dyns.iter() {
for &(_key, val, span) in dyns.iter() { self.emit_u8(KEY_DYNAMIC);
self.emit_maybe_thunk(val); let key_operand = self.classify_value(key);
self.emit_inline_operand(key_operand);
let val_operand = self.classify_value(val);
self.emit_inline_operand(val_operand);
let span_id = self.ctx.register_span(span); let span_id = self.ctx.register_span(span);
self.emit_u32(span_id); self.emit_u32(span_id);
} }
@@ -694,102 +869,69 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
span: TextRange, span: TextRange,
) { ) {
self.emit_expr(expr); self.emit_expr(expr);
for attr in attrpath.iter() {
let mut dynamic_patches = Vec::new(); if let Attr::Dynamic(expr, _) = *attr {
self.emit_expr(expr);
}
}
if let Some(default) = default {
self.emit_expr(default);
let span_id = self.ctx.register_span(span);
self.emit_op(Op::SelectDefault);
self.emit_u16(attrpath.len() as u16);
self.emit_u32(span_id);
} else {
let span_id = self.ctx.register_span(span);
self.emit_op(Op::Select);
self.emit_u16(attrpath.len() as u16);
self.emit_u32(span_id);
}
for attr in attrpath.iter() { for attr in attrpath.iter() {
match *attr { match *attr {
Attr::Str(sym, _) => { Attr::Str(sym, _) => {
let span_id = self.ctx.register_span(span); self.emit_u8(KEY_STATIC);
self.emit_op(Op::SelectStatic);
self.emit_u32(span_id);
self.emit_str_id(sym); self.emit_str_id(sym);
} }
Attr::Dynamic(key_expr, _) => { Attr::Dynamic(_, _) => {
self.emit_op(Op::JumpIfSelectFailed); self.emit_u8(KEY_DYNAMIC);
dynamic_patches.push(self.emit_i32_placeholder());
self.emit_expr(key_expr);
let span_id = self.ctx.register_span(span);
self.emit_op(Op::SelectDynamic);
self.emit_u32(span_id);
} }
} }
} }
if let Some(default) = default {
// FIXME: i32???
let before: i32 = self
.ctx
.get_code()
.len()
.try_into()
.expect("emitted code length fits in i32");
for patch in dynamic_patches {
self.patch_jump_target(patch);
}
self.emit_op(Op::JumpIfSelectSucceeded);
let placeholder = self.emit_i32_placeholder();
self.emit_expr(default);
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
// size of JumpIfSelectSucceeded (1) + placeholder (4).
self.patch_i32(placeholder, after - before - 5);
} else {
for patch in dynamic_patches {
self.patch_jump_target(patch);
}
}
} }
#[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);
for attr in rhs.iter() {
let mut dynamic_patches = Vec::new(); if let Attr::Dynamic(expr, _) = *attr {
let [attrs @ .., last] = rhs else { self.emit_expr(expr);
panic!("attrpath is empty"); }
}; }
for attr in attrs { self.emit_op(Op::HasAttr);
self.emit_u16(rhs.len() as u16);
for attr in rhs.iter() {
match *attr { match *attr {
Attr::Str(sym, span) => { Attr::Str(sym, _) => {
let span_id = self.ctx.register_span(span); self.emit_u8(KEY_STATIC);
self.emit_op(Op::HasAttrPathStatic);
self.emit_u32(span_id);
self.emit_str_id(sym); self.emit_str_id(sym);
} }
Attr::Dynamic(key_expr, span) => { Attr::Dynamic(_, _) => {
self.emit_op(Op::JumpIfSelectFailed); self.emit_u8(KEY_DYNAMIC);
dynamic_patches.push(self.emit_i32_placeholder());
self.emit_expr(key_expr);
let span_id = self.ctx.register_span(span);
self.emit_op(Op::HasAttrPathDynamic);
self.emit_u32(span_id);
} }
} }
} }
match *last { }
Attr::Str(sym, _) => {
self.emit_op(Op::HasAttrStatic); fn emit_with(
self.emit_str_id(sym); &mut self,
} namespace: RawIrRef<'_>,
Attr::Dynamic(key_expr, _) => { body: RawIrRef<'_>,
self.emit_op(Op::JumpIfSelectFailed); thunks: &[(ThunkId, RawIrRef<'_>)],
dynamic_patches.push(self.emit_i32_placeholder()); ) {
self.emit_expr(key_expr); self.emit_expr(namespace);
self.emit_op(Op::HasAttrDynamic); self.emit_op(Op::PushWith);
} self.emit_scope_thunks(thunks);
} self.emit_expr(body);
for patch in dynamic_patches { self.emit_op(Op::PopWith);
self.patch_jump_target(patch);
}
self.emit_op(Op::HasAttrResolve);
} }
fn emit_toplevel_inner(&mut self, body: RawIrRef<'_>, thunks: &[(ThunkId, RawIrRef<'_>)]) { fn emit_toplevel_inner(&mut self, body: RawIrRef<'_>, thunks: &[(ThunkId, RawIrRef<'_>)]) {
+363
View File
@@ -0,0 +1,363 @@
use std::fmt::Write;
use colored::Colorize;
use num_enum::TryFromPrimitive;
use crate::codegen::{InstructionPtr, Op};
pub(crate) trait DisassemblerContext {
fn lookup_string(&self, id: u32) -> &str;
fn get_code(&self) -> &[u8];
}
pub(crate) struct Disassembler<'a, Ctx> {
code: &'a [u8],
ctx: &'a Ctx,
pc: usize,
}
impl<'a, Ctx: DisassemblerContext> Disassembler<'a, Ctx> {
pub fn new(ip: InstructionPtr, ctx: &'a Ctx) -> Self {
Self {
code: ctx.get_code(),
ctx,
pc: ip.0,
}
}
fn read_u8(&mut self) -> u8 {
let b = self.code[self.pc];
self.pc += 1;
b
}
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)
}
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)
}
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)
}
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)
}
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)
}
pub fn disassemble(&mut self) -> String {
self.disassemble_impl(false)
}
pub fn disassemble_colored(&mut self) -> String {
self.disassemble_impl(true)
}
fn disassemble_impl(&mut self, color: bool) -> String {
let mut out = String::new();
if color {
let _ = writeln!(out, "{}", "=== Bytecode Disassembly ===".bold().white());
let _ = writeln!(
out,
"{} {}",
"Length:".white(),
format!("{} bytes", self.code.len()).cyan()
);
} else {
let _ = writeln!(out, "=== Bytecode Disassembly ===");
let _ = writeln!(out, "Length: {} bytes", self.code.len());
}
while self.pc < self.code.len() {
let start_pos = self.pc;
let op_byte = self.read_u8();
let (mnemonic, args) = self.decode_instruction(op_byte, start_pos);
let bytes_slice = &self.code[start_pos + 1..self.pc];
let mut chunks = bytes_slice.chunks(4);
let first_chunk = chunks.next().unwrap_or(&[]);
let bytes_str = {
let mut temp = format!("{:02x}", self.code[start_pos]);
for b in first_chunk {
let _ = write!(&mut temp, " {:02x}", b);
}
temp
};
if color {
let sep = if args.is_empty() { "" } else { " " };
let _ = writeln!(
out,
"{} {:<14} | {}{}{}",
format!("{:04x}", start_pos).dimmed(),
bytes_str.green(),
mnemonic.yellow().bold(),
sep,
args.cyan()
);
} else {
let op_str = if args.is_empty() {
mnemonic.to_string()
} else {
format!("{} {}", mnemonic, args)
};
let _ = writeln!(out, "{:04x} {:<14} | {}", start_pos, bytes_str, op_str);
}
for chunk in chunks {
let bytes_str = {
let mut temp = String::from(" ");
for b in chunk {
let _ = write!(&mut temp, " {:02x}", b);
}
temp
};
let extra_width = if start_pos > 0 { start_pos.ilog2() >> 4 } else { 0 };
if color {
let _ = write!(out, " ");
for _ in 0..extra_width { let _ = write!(out, " "); }
let _ = writeln!(out, " {:<14} |", bytes_str.green());
} else {
let _ = write!(out, " ");
for _ in 0..extra_width { let _ = write!(out, " "); }
let _ = writeln!(out, " {:<14} |", bytes_str);
}
}
}
out
}
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");
match op {
Op::PushSmi => {
let val = self.read_i32();
("PushSmi", format!("{}", val))
}
Op::PushBigInt => {
let val = self.read_i64();
("PushBigInt", format!("{}", val))
}
Op::PushFloat => {
let val = self.read_f64();
("PushFloat", format!("{}", val))
}
Op::PushString => {
let idx = self.read_u32();
let s = self.ctx.lookup_string(idx);
let len = s.len();
let mut s_fmt = format!("{:?}", s);
if s_fmt.len() > 60 {
s_fmt.truncate(57);
#[allow(clippy::unwrap_used)]
write!(s_fmt, "...\" (total {len} bytes)").unwrap();
}
("PushString", format!("@{} {}", idx, s_fmt))
}
Op::PushNull => ("PushNull", String::new()),
Op::PushTrue => ("PushTrue", String::new()),
Op::PushFalse => ("PushFalse", String::new()),
Op::LoadLocal => {
let idx = self.read_u32();
("LoadLocal", format!("[{}]", idx))
}
Op::LoadOuter => {
let depth = self.read_u8();
let idx = self.read_u32();
("LoadOuter", format!("depth={} [{}]", depth, idx))
}
Op::StoreLocal => {
let idx = self.read_u32();
("StoreLocal", format!("[{}]", idx))
}
Op::AllocLocals => {
let count = self.read_u32();
("AllocLocals", format!("count={}", count))
}
Op::MakeThunk => {
let offset = self.read_u32();
let label_idx = self.read_u32();
let label = self.ctx.lookup_string(label_idx);
("MakeThunk", format!("-> {:04x} label={}", offset, label))
}
Op::MakeClosure => {
let offset = self.read_u32();
let slots = self.read_u32();
("MakeClosure", format!("-> {:04x} slots={}", offset, slots))
}
Op::MakePatternClosure => {
let offset = self.read_u32();
let slots = self.read_u32();
let req_count = self.read_u16();
let opt_count = self.read_u16();
let ellipsis = self.read_u8() != 0;
let mut arg_str = format!(
"-> {:04x} slots={} req={} opt={} ...={})",
offset, slots, req_count, opt_count, ellipsis
);
arg_str.push_str(" Args=[");
for _ in 0..req_count {
let idx = self.read_u32();
arg_str.push_str(&format!("Req({}) ", self.ctx.lookup_string(idx)));
}
for _ in 0..opt_count {
let idx = self.read_u32();
arg_str.push_str(&format!("Opt({}) ", self.ctx.lookup_string(idx)));
}
let total_args = req_count + opt_count;
for _ in 0..total_args {
let _name_idx = self.read_u32();
let _span_id = self.read_u32();
}
arg_str.push(']');
("MakePatternClosure", arg_str)
}
Op::Call => {
let span_id = self.read_u32();
("Call", format!("span={}", span_id))
}
Op::CallNoSpan => ("CallNoSpan", String::new()),
Op::MakeAttrs => {
let count = self.read_u32();
("MakeAttrs", format!("size={}", count))
}
Op::MakeEmptyAttrs => ("MakeEmptyAttrs", String::new()),
Op::Select => {
let path_len = self.read_u16();
let span_id = self.read_u32();
("Select", format!("path_len={} span={}", path_len, span_id))
}
Op::SelectDefault => {
let path_len = self.read_u16();
let span_id = self.read_u32();
(
"SelectDefault",
format!("path_len={} span={}", path_len, span_id),
)
}
Op::HasAttr => {
let path_len = self.read_u16();
("HasAttr", format!("path_len={}", path_len))
}
Op::MakeList => {
let count = self.read_u32();
("MakeList", format!("size={}", count))
}
Op::MakeEmptyList => ("MakeEmptyList", String::new()),
Op::OpAdd => ("OpAdd", String::new()),
Op::OpSub => ("OpSub", String::new()),
Op::OpMul => ("OpMul", String::new()),
Op::OpDiv => ("OpDiv", String::new()),
Op::OpEq => ("OpEq", String::new()),
Op::OpNeq => ("OpNeq", String::new()),
Op::OpLt => ("OpLt", String::new()),
Op::OpGt => ("OpGt", String::new()),
Op::OpLeq => ("OpLeq", String::new()),
Op::OpGeq => ("OpGeq", String::new()),
Op::OpConcat => ("OpConcat", String::new()),
Op::OpUpdate => ("OpUpdate", String::new()),
Op::OpNeg => ("OpNeg", String::new()),
Op::OpNot => ("OpNot", String::new()),
Op::JumpIfFalse => {
let offset = self.read_i32();
let target = (current_pc as isize + 1 + 4 + offset as isize) as usize;
(
"JumpIfFalse",
format!("-> {:04x} offset={}", target, offset),
)
}
Op::JumpIfTrue => {
let offset = self.read_i32();
let target = (current_pc as isize + 1 + 4 + offset as isize) as usize;
("JumpIfTrue", format!("-> {:04x} offset={}", target, offset))
}
Op::Jump => {
let offset = self.read_i32();
let target = (current_pc as isize + 1 + 4 + offset as isize) as usize;
("Jump", format!("-> {:04x} offset={}", target, offset))
}
Op::ConcatStrings => {
let count = self.read_u16();
let force = self.read_u8();
("ConcatStrings", format!("count={} force={}", count, force))
}
Op::ResolvePath => ("ResolvePath", String::new()),
Op::Assert => {
let raw_idx = self.read_u32();
let span_id = self.read_u32();
("Assert", format!("text_id={} span={}", raw_idx, span_id))
}
Op::PushWith => ("PushWith", String::new()),
Op::PopWith => ("PopWith", String::new()),
Op::WithLookup => {
let idx = self.read_u32();
let name = self.ctx.lookup_string(idx);
("WithLookup", format!("{:?}", name))
}
Op::LoadBuiltins => ("LoadBuiltins", String::new()),
Op::LoadBuiltin => {
let id = self.read_u8();
("LoadBuiltin", format!("id={}", id))
}
Op::MkPos => {
let span_id = self.read_u32();
("MkPos", format!("id={}", span_id))
}
Op::LoadReplBinding => {
let idx = self.read_u32();
let name = self.ctx.lookup_string(idx);
("LoadReplBinding", format!("{:?}", name))
}
Op::LoadScopedBinding => {
let idx = self.read_u32();
let name = self.ctx.lookup_string(idx);
("LoadScopedBinding", format!("{:?}", name))
}
Op::Return => ("Return", String::new()),
}
}
}
@@ -1,13 +1,15 @@
use bumpalo::boxed::Box;
use bumpalo::collections::{CollectIn, Vec}; use bumpalo::collections::{CollectIn, Vec};
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;
use rnix::ast::{self, AstToken, Expr, HasEntry}; use rnix::ast::{self, Expr, HasEntry};
use rowan::ast::AstNode; use rowan::ast::AstNode;
use super::*; use crate::error::{Error, Result, Source};
use crate::ir::*;
use crate::runtime::BuiltinId;
use crate::value::Symbol;
trait Require<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>, T> { trait Require<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>, T> {
fn require(self, ctx: &Ctx, span: TextRange) -> Result<T>; fn require(self, ctx: &Ctx, span: TextRange) -> Result<T>;
@@ -37,26 +39,34 @@ impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>, T, E: std::fmt::Display>
} }
} }
pub trait DowngradeContext<'id: 'ir, 'ir> { trait BoxIn: Sized {
fn new_expr(&self, expr: Ir<'ir, GhostRoRef<'id, 'ir>>) -> GhostRoIrRef<'id, 'ir>; fn box_in(self, bump: &bumpalo::Bump) -> Box<'_, Self> {
fn maybe_thunk(&mut self, ir: GhostRoIrRef<'id, 'ir>) -> GhostRoMaybeThunkRef<'id, 'ir>; Box::new_in(self, bump)
}
}
impl<T: Sized> BoxIn for T {}
fn intern_string(&mut self, sym: impl AsRef<str>) -> StringId; pub trait DowngradeContext<'id: 'ir, 'ir> {
fn resolve_sym(&self, id: StringId) -> Symbol<'_>; fn new_expr(&self, expr: Ir<'ir, IrRef<'id, 'ir>>) -> IrRef<'id, 'ir>;
fn lookup(&mut self, sym: StringId, span: TextRange) -> Result<GhostRoMaybeThunkRef<'id, 'ir>>; fn new_arg(&mut self) -> ArgId;
fn maybe_thunk(&mut self, ir: IrRef<'id, 'ir>) -> IrRef<'id, 'ir>;
fn new_sym(&mut self, sym: String) -> StringId;
fn get_sym(&self, id: StringId) -> Symbol<'_>;
fn lookup(&self, sym: StringId, span: TextRange) -> Result<IrRef<'id, 'ir>>;
fn get_current_source(&self) -> Source; fn get_current_source(&self) -> Source;
fn with_param_scope<F, R>(&mut self, param: StringId, f: F) -> R fn with_param_scope<F, R>(&mut self, param: StringId, arg: ArgId, f: F) -> R
where where
F: FnOnce(&mut Self) -> R; F: FnOnce(&mut Self) -> R;
fn with_let_scope<F, R>(&mut self, bindings: &[StringId], f: F) -> Result<R> fn with_let_scope<F, R>(&mut self, bindings: &[StringId], f: F) -> Result<R>
where where
F: FnOnce(&mut Self) -> Result<(Vec<'ir, GhostRoMaybeThunkRef<'id, 'ir>>, R)>; F: FnOnce(&mut Self) -> Result<(Vec<'ir, IrRef<'id, 'ir>>, R)>;
fn with_with_scope<F, R>(&mut self, namespace: GhostRoMaybeThunkRef<'id, 'ir>, f: F) -> R fn with_with_scope<F, R>(&mut self, f: F) -> R
where where
F: FnOnce(&mut Self) -> R; F: FnOnce(&mut Self) -> R;
fn with_thunk_scope<F, R>(&mut self, f: F) -> (R, Vec<'ir, (ThunkId, GhostRoIrRef<'id, 'ir>)>) fn with_thunk_scope<F, R>(&mut self, f: F) -> (R, Vec<'ir, (ThunkId, IrRef<'id, 'ir>)>)
where where
F: FnOnce(&mut Self) -> R; F: FnOnce(&mut Self) -> R;
@@ -64,11 +74,11 @@ pub trait DowngradeContext<'id: 'ir, 'ir> {
} }
pub trait Downgrade<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> { pub trait Downgrade<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> {
fn downgrade(self, ctx: &mut Ctx) -> Result<GhostRoIrRef<'id, 'ir>>; fn downgrade(self, ctx: &mut Ctx) -> Result<IrRef<'id, 'ir>>;
} }
impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for Expr { impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for Expr {
fn downgrade(self, ctx: &mut Ctx) -> Result<GhostRoIrRef<'id, 'ir>> { fn downgrade(self, ctx: &mut Ctx) -> Result<IrRef<'id, 'ir>> {
use Expr::*; use Expr::*;
match self { match self {
Apply(apply) => apply.downgrade(ctx), Apply(apply) => apply.downgrade(ctx),
@@ -97,7 +107,7 @@ impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> fo
AttrSet(attrs) => attrs.downgrade(ctx), AttrSet(attrs) => attrs.downgrade(ctx),
UnaryOp(op) => op.downgrade(ctx), UnaryOp(op) => op.downgrade(ctx),
Ident(ident) => ident.downgrade(ctx), Ident(ident) => ident.downgrade(ctx),
CurPos(curpos) => curpos.downgrade(ctx), CurPos(curpos) => Ok(ctx.new_expr(Ir::CurPos(curpos.syntax().text_range()))),
With(with) => with.downgrade(ctx), With(with) => with.downgrade(ctx),
HasAttr(has) => has.downgrade(ctx), HasAttr(has) => has.downgrade(ctx),
Paren(paren) => paren Paren(paren) => paren
@@ -113,7 +123,7 @@ impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> fo
} }
impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::Assert { impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::Assert {
fn downgrade(self, ctx: &mut Ctx) -> Result<GhostRoIrRef<'id, 'ir>> { fn downgrade(self, ctx: &mut Ctx) -> Result<IrRef<'id, 'ir>> {
let span = self.syntax().text_range(); let span = self.syntax().text_range();
let assertion = self.condition().require(ctx, span)?; let assertion = self.condition().require(ctx, span)?;
let assertion_raw = assertion.to_string(); let assertion_raw = assertion.to_string();
@@ -129,7 +139,7 @@ impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> fo
} }
impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::IfElse { impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::IfElse {
fn downgrade(self, ctx: &mut Ctx) -> Result<GhostRoIrRef<'id, 'ir>> { fn downgrade(self, ctx: &mut Ctx) -> Result<IrRef<'id, 'ir>> {
let span = self.syntax().text_range(); let span = self.syntax().text_range();
let cond = self.condition().require(ctx, span)?.downgrade(ctx)?; let cond = self.condition().require(ctx, span)?.downgrade(ctx)?;
let consq = self.body().require(ctx, span)?.downgrade(ctx)?; let consq = self.body().require(ctx, span)?.downgrade(ctx)?;
@@ -141,8 +151,9 @@ impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> fo
macro_rules! path { macro_rules! path {
($ty:ident) => { ($ty:ident) => {
impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::$ty { impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::$ty {
fn downgrade(self, ctx: &mut Ctx) -> Result<GhostRoIrRef<'id, 'ir>> { fn downgrade(self, ctx: &mut Ctx) -> Result<IrRef<'id, 'ir>> {
downgrade_path(self.parts(), ctx) let span = self.syntax().text_range();
downgrade_path(self.parts(), span, ctx)
} }
} }
}; };
@@ -151,24 +162,19 @@ path!(PathAbs);
path!(PathRel); path!(PathRel);
path!(PathHome); path!(PathHome);
impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::PathSearch { impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::PathSearch {
fn downgrade(self, ctx: &mut Ctx) -> Result<GhostRoIrRef<'id, 'ir>> { fn downgrade(self, ctx: &mut Ctx) -> Result<IrRef<'id, 'ir>> {
let span = self.syntax().text_range(); let span = self.syntax().text_range();
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( ctx.new_expr(Ir::Str(
clippy::string_slice, text[1..text.len() - 1].to_string().box_in(ctx.bump()),
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 expr = ctx.new_expr(Ir::Str(id));
ctx.maybe_thunk(expr)
}; };
// HACK: disgusting eww // HACK: disgusting eww
let find_file = ctx.new_expr(Ir::Builtin(BuiltinId::FindFile)); let find_file = ctx.new_expr(Ir::Builtin(BuiltinId::FindFile));
let sym = ctx.intern_string("nixPath"); let sym = ctx.new_sym("nixPath".into());
let nix_path = ctx.new_expr(Ir::BuiltinConst(sym)); let nix_path = ctx.new_expr(Ir::BuiltinConst(sym));
let nix_path = ctx.maybe_thunk(nix_path);
let call = ctx.new_expr(Ir::Call { let call = ctx.new_expr(Ir::Call {
func: find_file, func: find_file,
arg: nix_path, arg: nix_path,
@@ -183,27 +189,29 @@ impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> fo
} }
impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::Str { impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::Str {
fn downgrade(self, ctx: &mut Ctx) -> Result<GhostRoIrRef<'id, 'ir>> { fn downgrade(self, ctx: &mut Ctx) -> Result<IrRef<'id, 'ir>> {
let normalized = self.normalized_parts(); let normalized = self.normalized_parts();
let is_single_literal = normalized.len() == 1 let is_single_literal = normalized.len() == 1
&& matches!(normalized.first(), Some(ast::InterpolPart::Literal(_))); && matches!(normalized.first(), Some(ast::InterpolPart::Literal(_)));
let bump = ctx.bump(); let bump = ctx.bump();
let mut parts = normalized.into_iter().map(|part| match part { let parts = normalized
ast::InterpolPart::Literal(lit) => { .into_iter()
let id = ctx.intern_string(lit); .map(|part| match part {
Ok(ctx.new_expr(Ir::Str(id))) ast::InterpolPart::Literal(lit) => Ok(ctx.new_expr(Ir::Str(lit.box_in(bump)))),
} ast::InterpolPart::Interpolation(interpol) => {
ast::InterpolPart::Interpolation(interpol) => interpol let inner = interpol
.expr() .expr()
.require(ctx, interpol.syntax().text_range())? .require(ctx, interpol.syntax().text_range())?
.downgrade(ctx), .downgrade(ctx)?;
}); Ok(ctx.maybe_thunk(inner))
}
})
.collect_in::<Result<Vec<'ir, _>>>(bump)?;
Ok(if is_single_literal { Ok(if is_single_literal {
parts.next().expect("is_single_literal checked")? parts.into_iter().next().expect("is_single_literal checked")
} else { } else {
let parts = parts.collect_in::<Result<_>>(bump)?;
ctx.new_expr(Ir::ConcatStrings { ctx.new_expr(Ir::ConcatStrings {
parts, parts,
force_string: true, force_string: true,
@@ -213,88 +221,27 @@ impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> fo
} }
impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::Literal { impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::Literal {
fn downgrade(self, ctx: &mut Ctx) -> Result<GhostRoIrRef<'id, 'ir>> { fn downgrade(self, ctx: &mut Ctx) -> Result<IrRef<'id, 'ir>> {
let span = self.syntax().text_range(); let span = self.syntax().text_range();
let expr = match self.kind() { Ok(ctx.new_expr(match self.kind() {
ast::LiteralKind::Integer(int) => Ir::Int(int.value().require(ctx, span)?), ast::LiteralKind::Integer(int) => Ir::Int(int.value().require(ctx, span)?),
ast::LiteralKind::Float(float) => Ir::Float(float.value().require(ctx, span)?), ast::LiteralKind::Float(float) => Ir::Float(float.value().require(ctx, span)?),
ast::LiteralKind::Uri(uri) => { ast::LiteralKind::Uri(uri) => Ir::Str(uri.to_string().box_in(ctx.bump())),
let id = ctx.intern_string(uri.syntax().text());
Ir::Str(id)
}
};
Ok(ctx.new_expr(expr))
}
}
impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::Ident {
fn downgrade(self, ctx: &mut Ctx) -> Result<GhostRoIrRef<'id, 'ir>> {
let span = self.syntax().text_range();
let text = self.ident_token().require(ctx, span)?.to_string();
let sym = ctx.intern_string(text);
ctx.lookup(sym, span)
.map(|thunk| ctx.new_expr(Ir::MaybeThunk(thunk)))
}
}
impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::CurPos {
fn downgrade(self, ctx: &mut Ctx) -> Result<GhostRoIrRef<'id, 'ir>> {
fn byte_offset_to_line_col(content: &str, offset: usize) -> (u32, u32) {
let mut line = 1u32;
let mut col = 1u32;
for (idx, ch) in content.char_indices() {
if idx >= offset {
break;
}
if ch == '\n' {
line += 1;
col = 1;
} else {
col += 1;
}
}
(line, col)
}
let span = self.syntax().text_range();
let source = ctx.get_current_source();
let (line, column) = byte_offset_to_line_col(&source.src, span.start().into());
let file_sym = ctx.intern_string("file");
let line_sym = ctx.intern_string("line");
let column_sym = ctx.intern_string("column");
let file: GhostRoMaybeThunkRef = ctx
.bump()
.alloc(GhostCell::new(MaybeThunk::Str(ctx.intern_string(source.get_name()))).into());
let line = ctx
.bump()
.alloc(GhostCell::new(MaybeThunk::Int(i64::from(line))).into());
let column = ctx
.bump()
.alloc(GhostCell::new(MaybeThunk::Int(i64::from(column))).into());
let map = {
let mut map = HashMap::new_in(ctx.bump());
map.insert(file_sym, (file, TextRange::default()));
map.insert(line_sym, (line, TextRange::default()));
map.insert(column_sym, (column, TextRange::default()));
map
};
Ok(ctx.new_expr(Ir::AttrSet {
stcs: map,
dyns: Vec::new_in(ctx.bump()),
})) }))
} }
} }
impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::Ident {
fn downgrade(self, ctx: &mut Ctx) -> Result<IrRef<'id, 'ir>> {
let span = self.syntax().text_range();
let text = self.ident_token().require(ctx, span)?.to_string();
let sym = ctx.new_sym(text);
ctx.lookup(sym, span)
}
}
impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::AttrSet { impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::AttrSet {
fn downgrade(self, ctx: &mut Ctx) -> Result<GhostRoIrRef<'id, 'ir>> { fn downgrade(self, ctx: &mut Ctx) -> Result<IrRef<'id, 'ir>> {
let rec = self.rec_token().is_some(); let rec = self.rec_token().is_some();
if !rec { if !rec {
let attrs = downgrade_attrs(self, ctx)?; let attrs = downgrade_attrs(self, ctx)?;
@@ -310,7 +257,7 @@ impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> fo
} }
impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::List { impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::List {
fn downgrade(self, ctx: &mut Ctx) -> Result<GhostRoIrRef<'id, 'ir>> { fn downgrade(self, ctx: &mut Ctx) -> Result<IrRef<'id, 'ir>> {
let bump = ctx.bump(); let bump = ctx.bump();
let items = self let items = self
.items() .items()
@@ -324,52 +271,17 @@ impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> fo
} }
impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::BinOp { impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::BinOp {
fn downgrade(self, ctx: &mut Ctx) -> Result<GhostRoIrRef<'id, 'ir>> { fn downgrade(self, ctx: &mut Ctx) -> Result<IrRef<'id, 'ir>> {
use BinOpKind::*;
use ast::BinOpKind as Kind;
let span = self.syntax().text_range(); let span = self.syntax().text_range();
let lhs = self.lhs().require(ctx, span)?.downgrade(ctx)?; let lhs = self.lhs().require(ctx, span)?.downgrade(ctx)?;
let rhs = self.rhs().require(ctx, span)?.downgrade(ctx)?; let rhs = self.rhs().require(ctx, span)?.downgrade(ctx)?;
let kind = match self.operator().require(ctx, span)? { let kind = self.operator().require(ctx, span)?.into();
Kind::Concat => Con,
Kind::Update => Upd,
Kind::Add => Add,
Kind::Sub => Sub,
Kind::Mul => Mul,
Kind::Div => Div,
Kind::And => And,
Kind::Equal => Eq,
Kind::Implication => Impl,
Kind::Less => Lt,
Kind::LessOrEq => Leq,
Kind::More => Gt,
Kind::MoreOrEq => Geq,
Kind::NotEqual => Neq,
Kind::Or => Or,
Kind::PipeLeft => {
let arg = ctx.maybe_thunk(rhs);
return Ok(ctx.new_expr(Ir::Call {
func: lhs,
arg,
span,
}));
}
Kind::PipeRight => {
let arg = ctx.maybe_thunk(lhs);
return Ok(ctx.new_expr(Ir::Call {
func: rhs,
arg,
span,
}));
}
};
Ok(ctx.new_expr(Ir::BinOp { lhs, rhs, kind })) Ok(ctx.new_expr(Ir::BinOp { lhs, rhs, kind }))
} }
} }
impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::HasAttr { impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::HasAttr {
fn downgrade(self, ctx: &mut Ctx) -> Result<GhostRoIrRef<'id, 'ir>> { fn downgrade(self, ctx: &mut Ctx) -> Result<IrRef<'id, 'ir>> {
let span = self.syntax().text_range(); let span = self.syntax().text_range();
let lhs = self.expr().require(ctx, span)?.downgrade(ctx)?; let lhs = self.expr().require(ctx, span)?.downgrade(ctx)?;
let rhs = downgrade_attrpath(self.attrpath().require(ctx, span)?, ctx)?; let rhs = downgrade_attrpath(self.attrpath().require(ctx, span)?, ctx)?;
@@ -378,7 +290,7 @@ impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> fo
} }
impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::UnaryOp { impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::UnaryOp {
fn downgrade(self, ctx: &mut Ctx) -> Result<GhostRoIrRef<'id, 'ir>> { fn downgrade(self, ctx: &mut Ctx) -> Result<IrRef<'id, 'ir>> {
let span = self.syntax().text_range(); let span = self.syntax().text_range();
let rhs = self.expr().require(ctx, span)?.downgrade(ctx)?; let rhs = self.expr().require(ctx, span)?.downgrade(ctx)?;
let kind = self.operator().require(ctx, span)?.into(); let kind = self.operator().require(ctx, span)?.into();
@@ -387,14 +299,16 @@ impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> fo
} }
impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::Select { impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::Select {
fn downgrade(self, ctx: &mut Ctx) -> Result<GhostRoIrRef<'id, 'ir>> { fn downgrade(self, ctx: &mut Ctx) -> Result<IrRef<'id, 'ir>> {
let span = self.syntax().text_range(); let span = self.syntax().text_range();
let expr = self.expr().require(ctx, span)?.downgrade(ctx)?; let expr = self.expr().require(ctx, span)?.downgrade(ctx)?;
let attrpath = downgrade_attrpath(self.attrpath().require(ctx, span)?, ctx)?; let attrpath = downgrade_attrpath(self.attrpath().require(ctx, span)?, ctx)?;
let default = self let default = if let Some(default) = self.default_expr() {
.default_expr() let default_expr = default.downgrade(ctx)?;
.map(|expr| expr.downgrade(ctx)) Some(ctx.maybe_thunk(default_expr))
.transpose()?; } else {
None
};
let span = self.syntax().text_range(); let span = self.syntax().text_range();
Ok(ctx.new_expr(Ir::Select { Ok(ctx.new_expr(Ir::Select {
expr, expr,
@@ -406,7 +320,7 @@ impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> fo
} }
impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::LegacyLet { impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::LegacyLet {
fn downgrade(self, ctx: &mut Ctx) -> Result<GhostRoIrRef<'id, 'ir>> { fn downgrade(self, ctx: &mut Ctx) -> Result<IrRef<'id, 'ir>> {
let span = self.syntax().text_range(); let span = self.syntax().text_range();
let entries: Vec<'ir, _> = self.entries().collect_in(ctx.bump()); let entries: Vec<'ir, _> = self.entries().collect_in(ctx.bump());
let attrset_expr = downgrade_let_bindings(entries, ctx, |ctx, binding_keys| { let attrset_expr = downgrade_let_bindings(entries, ctx, |ctx, binding_keys| {
@@ -421,7 +335,7 @@ impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> fo
Ok(ctx.new_expr(Ir::AttrSet { stcs, dyns })) Ok(ctx.new_expr(Ir::AttrSet { stcs, dyns }))
})?; })?;
let body_sym = ctx.intern_string("body"); let body_sym = ctx.new_sym("body".to_string());
Ok(ctx.new_expr(Ir::Select { Ok(ctx.new_expr(Ir::Select {
expr: attrset_expr, expr: attrset_expr,
attrpath: Vec::from_iter_in( attrpath: Vec::from_iter_in(
@@ -435,7 +349,7 @@ impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> fo
} }
impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::LetIn { impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::LetIn {
fn downgrade(self, ctx: &mut Ctx) -> Result<GhostRoIrRef<'id, 'ir>> { fn downgrade(self, ctx: &mut Ctx) -> Result<IrRef<'id, 'ir>> {
let entries: Vec<'ir, _> = self.entries().collect_in(ctx.bump()); let entries: Vec<'ir, _> = self.entries().collect_in(ctx.bump());
let span = self.syntax().text_range(); let span = self.syntax().text_range();
let body_expr = self.body().require(ctx, span)?; let body_expr = self.body().require(ctx, span)?;
@@ -445,42 +359,54 @@ impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> fo
} }
impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::With { impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::With {
fn downgrade(self, ctx: &mut Ctx) -> Result<GhostRoIrRef<'id, 'ir>> { fn downgrade(self, ctx: &mut Ctx) -> Result<IrRef<'id, 'ir>> {
let span = self.syntax().text_range(); let span = self.syntax().text_range();
let namespace = self.namespace().require(ctx, span)?.downgrade(ctx)?; let namespace = self.namespace().require(ctx, span)?.downgrade(ctx)?;
let namespace = ctx.maybe_thunk(namespace); let namespace = ctx.maybe_thunk(namespace);
let body_expr = self.body().require(ctx, span)?; let body_expr = self.body().require(ctx, span)?;
ctx.with_with_scope(namespace, |ctx| body_expr.downgrade(ctx)) let (body, thunks) =
ctx.with_thunk_scope(|ctx| ctx.with_with_scope(|ctx| body_expr.downgrade(ctx)));
let body = body?;
Ok(ctx.new_expr(Ir::With {
namespace,
body,
thunks,
}))
} }
} }
impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::Lambda { impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::Lambda {
fn downgrade(self, ctx: &mut Ctx) -> Result<GhostRoIrRef<'id, 'ir>> { fn downgrade(self, ctx: &mut Ctx) -> Result<IrRef<'id, 'ir>> {
let span = self.syntax().text_range(); let span = self.syntax().text_range();
let raw_param = self.param().require(ctx, span)?; let raw_param = self.param().require(ctx, span)?;
let body_ast = self.body().require(ctx, span)?; let body_ast = self.body().require(ctx, span)?;
let arg = ctx.new_arg();
struct Ret<'id, 'ir> { struct Ret<'id, 'ir> {
param: Option<Param<'ir>>, param: Option<Param<'ir>>,
body: GhostRoIrRef<'id, 'ir>, body: IrRef<'id, 'ir>,
} }
let (ret, thunks) = ctx.with_thunk_scope(|ctx| -> Result<Ret> { let (ret, thunks) = ctx.with_thunk_scope(|ctx| {
let (param, body) = match raw_param { let 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.new_sym(id.to_string());
( param = None;
None,
ctx.with_param_scope(param_sym, |ctx| body_ast.downgrade(ctx))?, body = ctx
) .with_param_scope(param_sym, arg, |ctx| body_ast.clone().downgrade(ctx))?;
} }
ast::Param::Pattern(pattern) => { ast::Param::Pattern(pattern) => {
let alias = pattern let alias = pattern
.pat_bind() .pat_bind()
.map(|alias| { .map(|alias| {
let ident = alias.ident().require(ctx, alias.syntax().text_range())?; let ident = alias.ident().require(ctx, alias.syntax().text_range())?;
Ok::<_, std::boxed::Box<Error>>(ctx.intern_string(ident.to_string())) Ok::<_, std::boxed::Box<Error>>(ctx.new_sym(ident.to_string()))
}) })
.transpose()?; .transpose()?;
@@ -491,35 +417,35 @@ impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> fo
body: inner_body, body: inner_body,
required, required,
optional, optional,
} = downgrade_pattern_bindings(pat_entries, alias, ctx, |ctx, _| { } = downgrade_pattern_bindings(pat_entries, alias, arg, ctx, |ctx, _| {
body_ast.clone().downgrade(ctx) body_ast.clone().downgrade(ctx)
})?; })?;
( param = Some(Param {
Some(Param { required,
required, optional,
optional, ellipsis,
ellipsis, });
}),
inner_body,
)
}
};
Ok(Ret { param, body }) body = inner_body;
}
}
Result::Ok(Ret { param, body })
}); });
let Ret { param, body } = ret?; let Ret { param, body } = ret?;
Ok(ctx.new_expr(Ir::Func { Ok(ctx.new_expr(Ir::Func {
body, body,
param, param,
arg,
thunks, thunks,
})) }))
} }
} }
impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::Apply { impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> for ast::Apply {
fn downgrade(self, ctx: &mut Ctx) -> Result<GhostRoIrRef<'id, 'ir>> { fn downgrade(self, ctx: &mut Ctx) -> Result<IrRef<'id, 'ir>> {
let span = self.syntax().text_range(); let span = self.syntax().text_range();
let func = self.lambda().require(ctx, span)?.downgrade(ctx)?; let func = self.lambda().require(ctx, span)?.downgrade(ctx)?;
let arg = self.argument().require(ctx, span)?.downgrade(ctx)?; let arg = self.argument().require(ctx, span)?.downgrade(ctx)?;
@@ -553,10 +479,6 @@ 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],
@@ -569,7 +491,7 @@ impl<'id: 'ir, 'ir> PendingAttrSet<'ir> {
match first { match first {
ast::Attr::Ident(ident) => { ast::Attr::Ident(ident) => {
let sym = ctx.intern_string(ident.to_string()); let sym = ctx.new_sym(ident.to_string());
self.insert_static(sym, span, rest, value, ctx) self.insert_static(sym, span, rest, value, ctx)
} }
ast::Attr::Str(string) => { ast::Attr::Str(string) => {
@@ -578,7 +500,7 @@ impl<'id: 'ir, 'ir> PendingAttrSet<'ir> {
&& let ast::InterpolPart::Literal(lit) = && let ast::InterpolPart::Literal(lit) =
parts.into_iter().next().expect("len checked") parts.into_iter().next().expect("len checked")
{ {
let sym = ctx.intern_string(lit); let sym = ctx.new_sym(lit);
return self.insert_static(sym, span, rest, value, ctx); return self.insert_static(sym, span, rest, value, ctx);
} }
self.insert_dynamic(first.clone(), span, rest, value, ctx) self.insert_dynamic(first.clone(), span, rest, value, ctx)
@@ -658,10 +580,6 @@ 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(),
@@ -676,10 +594,6 @@ 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>,
@@ -829,14 +743,14 @@ impl<'id: 'ir, 'ir> PendingAttrSet<'ir> {
for attr in inherit.attrs() { for attr in inherit.attrs() {
let span = attr.syntax().text_range(); let span = attr.syntax().text_range();
let sym = match &attr { let sym = match &attr {
ast::Attr::Ident(ident) => ctx.intern_string(ident.to_string()), ast::Attr::Ident(ident) => ctx.new_sym(ident.to_string()),
ast::Attr::Str(s) => { ast::Attr::Str(s) => {
let parts = s.normalized_parts(); let parts = s.normalized_parts();
if parts.len() == 1 if parts.len() == 1
&& let ast::InterpolPart::Literal(lit) = && let ast::InterpolPart::Literal(lit) =
parts.into_iter().next().expect("len checked") parts.into_iter().next().expect("len checked")
{ {
ctx.intern_string(lit) ctx.new_sym(lit)
} else { } else {
return Err(Error::downgrade_error( return Err(Error::downgrade_error(
"dynamic attributes not allowed in inherit".to_string(), "dynamic attributes not allowed in inherit".to_string(),
@@ -856,7 +770,7 @@ impl<'id: 'ir, 'ir> PendingAttrSet<'ir> {
if self.stcs.contains_key(&sym) { if self.stcs.contains_key(&sym) {
return Err(Error::downgrade_error( return Err(Error::downgrade_error(
format!("attribute '{}' already defined", ctx.resolve_sym(sym)), format!("attribute '{}' already defined", ctx.get_sym(sym)),
ctx.get_current_source(), ctx.get_current_source(),
span, span,
)); ));
@@ -933,15 +847,8 @@ fn make_attrpath_value_entry<'ir>(path: Vec<'ir, ast::Attr>, value: ast::Expr) -
} }
struct FinalizedAttrSet<'id, 'ir> { struct FinalizedAttrSet<'id, 'ir> {
stcs: HashMap<'ir, StringId, (GhostRoMaybeThunkRef<'id, 'ir>, TextRange)>, stcs: HashMap<'ir, StringId, (IrRef<'id, 'ir>, TextRange)>,
dyns: Vec< dyns: Vec<'ir, (IrRef<'id, 'ir>, IrRef<'id, 'ir>, TextRange)>,
'ir,
(
GhostRoIrRef<'id, 'ir>,
GhostRoMaybeThunkRef<'id, 'ir>,
TextRange,
),
>,
} }
fn downgrade_attrs<'id, 'ir>( fn downgrade_attrs<'id, 'ir>(
@@ -956,22 +863,22 @@ fn downgrade_attrs<'id, 'ir>(
fn downgrade_attr<'id, 'ir>( fn downgrade_attr<'id, 'ir>(
attr: ast::Attr, attr: ast::Attr,
ctx: &mut impl DowngradeContext<'id, 'ir>, ctx: &mut impl DowngradeContext<'id, 'ir>,
) -> Result<Attr<GhostRoIrRef<'id, 'ir>>> { ) -> Result<Attr<IrRef<'id, 'ir>>> {
use ast::Attr::*; use ast::Attr::*;
use ast::InterpolPart::*; use ast::InterpolPart::*;
match attr { match attr {
Ident(ident) => Ok(Attr::Str( Ident(ident) => Ok(Attr::Str(
ctx.intern_string(ident.to_string()), ctx.new_sym(ident.to_string()),
ident.syntax().text_range(), ident.syntax().text_range(),
)), )),
Str(string) => { Str(string) => {
let parts = string.normalized_parts(); let parts = string.normalized_parts();
let span = string.syntax().text_range(); let span = string.syntax().text_range();
if parts.is_empty() { if parts.is_empty() {
Ok(Attr::Str(ctx.intern_string(""), span)) Ok(Attr::Str(ctx.new_sym("".to_string()), span))
} else if parts.len() == 1 { } else if parts.len() == 1 {
match parts.into_iter().next().expect("len checked") { match parts.into_iter().next().expect("len checked") {
Literal(ident) => Ok(Attr::Str(ctx.intern_string(ident), span)), Literal(ident) => Ok(Attr::Str(ctx.new_sym(ident), span)),
Interpolation(interpol) => Ok(Attr::Dynamic( Interpolation(interpol) => Ok(Attr::Dynamic(
interpol interpol
.expr() .expr()
@@ -985,10 +892,7 @@ fn downgrade_attr<'id, 'ir>(
let parts = parts let parts = parts
.into_iter() .into_iter()
.map(|part| match part { .map(|part| match part {
Literal(lit) => { Literal(lit) => Ok(ctx.new_expr(Ir::Str(lit.box_in(bump)))),
let id = ctx.intern_string(lit);
Ok(ctx.new_expr(Ir::Str(id)))
}
Interpolation(interpol) => interpol Interpolation(interpol) => interpol
.expr() .expr()
.require(ctx, interpol.syntax().text_range())? .require(ctx, interpol.syntax().text_range())?
@@ -1017,7 +921,7 @@ fn downgrade_attr<'id, 'ir>(
fn downgrade_attrpath<'id, 'ir>( fn downgrade_attrpath<'id, 'ir>(
attrpath: ast::Attrpath, attrpath: ast::Attrpath,
ctx: &mut impl DowngradeContext<'id, 'ir>, ctx: &mut impl DowngradeContext<'id, 'ir>,
) -> Result<Vec<'ir, Attr<GhostRoIrRef<'id, 'ir>>>> { ) -> Result<Vec<'ir, Attr<IrRef<'id, 'ir>>>> {
let bump = ctx.bump(); let bump = ctx.bump();
attrpath attrpath
.attrs() .attrs()
@@ -1026,7 +930,7 @@ fn downgrade_attrpath<'id, 'ir>(
} }
struct PatternBindings<'id, 'ir> { struct PatternBindings<'id, 'ir> {
body: GhostRoIrRef<'id, 'ir>, body: IrRef<'id, 'ir>,
required: Vec<'ir, (StringId, TextRange)>, required: Vec<'ir, (StringId, TextRange)>,
optional: Vec<'ir, (StringId, TextRange)>, optional: Vec<'ir, (StringId, TextRange)>,
} }
@@ -1034,12 +938,14 @@ struct PatternBindings<'id, 'ir> {
fn downgrade_pattern_bindings<'id, 'ir, Ctx>( fn downgrade_pattern_bindings<'id, 'ir, Ctx>(
pat_entries: impl Iterator<Item = ast::PatEntry>, pat_entries: impl Iterator<Item = ast::PatEntry>,
alias: Option<StringId>, alias: Option<StringId>,
arg: ArgId,
ctx: &mut Ctx, ctx: &mut Ctx,
body_fn: impl FnOnce(&mut Ctx, &[StringId]) -> Result<GhostRoIrRef<'id, 'ir>>, body_fn: impl FnOnce(&mut Ctx, &[StringId]) -> Result<IrRef<'id, 'ir>>,
) -> Result<PatternBindings<'id, 'ir>> ) -> Result<PatternBindings<'id, 'ir>>
where where
Ctx: DowngradeContext<'id, 'ir>, Ctx: DowngradeContext<'id, 'ir>,
{ {
let arg = ctx.new_expr(Ir::Arg(arg));
struct Param { struct Param {
sym: StringId, sym: StringId,
sym_span: TextRange, sym_span: TextRange,
@@ -1053,13 +959,13 @@ where
for entry in pat_entries { for entry in pat_entries {
let ident = entry.ident().require(ctx, entry.syntax().text_range())?; let ident = entry.ident().require(ctx, entry.syntax().text_range())?;
let sym_span = ident.syntax().text_range(); let sym_span = ident.syntax().text_range();
let sym = ctx.intern_string(ident.syntax().text().to_string()); let sym = ctx.new_sym(ident.syntax().text().to_string());
let default = entry.default(); let default = entry.default();
let span = entry.syntax().text_range(); let span = entry.syntax().text_range();
if !seen_params.insert(sym) { if !seen_params.insert(sym) {
return Err(Error::downgrade_error( return Err(Error::downgrade_error(
format!("duplicate parameter '{}'", ctx.resolve_sym(sym)), format!("duplicate parameter '{}'", ctx.get_sym(sym)),
ctx.get_current_source(), ctx.get_current_source(),
span, span,
)); ));
@@ -1093,8 +999,6 @@ where
} }
} }
let arg = ctx.new_expr(Ir::Arg { layer: 0 });
let arg_thunk = ctx.maybe_thunk(arg);
ctx.with_let_scope(&keys, |ctx| { ctx.with_let_scope(&keys, |ctx| {
let vals = params let vals = params
.into_iter() .into_iter()
@@ -1105,17 +1009,21 @@ where
default, default,
span, span,
} = param; } = param;
let default = default.map(|default| default.downgrade(ctx)).transpose()?; let default = if let Some(default) = default {
let default = default.clone().downgrade(ctx)?;
Some(ctx.maybe_thunk(default))
} else {
None
};
let expr = ctx.new_expr(Ir::Select { Ok(ctx.new_expr(Ir::Select {
expr: arg, expr: arg,
attrpath: Vec::from_iter_in([Attr::Str(sym, sym_span)], bump), attrpath: Vec::from_iter_in([Attr::Str(sym, sym_span)], bump),
default, default,
span, span,
}); }))
Ok(ctx.maybe_thunk(expr))
}) })
.chain(alias.into_iter().map(|_| Ok(arg_thunk))) .chain(alias.into_iter().map(|_| Ok(arg)))
.collect_in::<Result<_>>(bump)?; .collect_in::<Result<_>>(bump)?;
let body = body_fn(ctx, &keys)?; let body = body_fn(ctx, &keys)?;
@@ -1135,10 +1043,10 @@ fn downgrade_let_bindings<'id, 'ir, Ctx, F>(
entries: Vec<'ir, ast::Entry>, entries: Vec<'ir, ast::Entry>,
ctx: &mut Ctx, ctx: &mut Ctx,
body_fn: F, body_fn: F,
) -> Result<GhostRoIrRef<'id, 'ir>> ) -> Result<IrRef<'id, 'ir>>
where where
Ctx: DowngradeContext<'id, 'ir>, Ctx: DowngradeContext<'id, 'ir>,
F: FnOnce(&mut Ctx, &[StringId]) -> Result<GhostRoIrRef<'id, 'ir>>, F: FnOnce(&mut Ctx, &[StringId]) -> Result<IrRef<'id, 'ir>>,
{ {
downgrade_rec_attrs_impl::<_, _, false>(entries, ctx, |ctx, binding_keys, _dyns| { downgrade_rec_attrs_impl::<_, _, false>(entries, ctx, |ctx, binding_keys, _dyns| {
body_fn(ctx, binding_keys) body_fn(ctx, binding_keys)
@@ -1148,7 +1056,7 @@ where
fn downgrade_rec_bindings<'id, 'ir, Ctx>( fn downgrade_rec_bindings<'id, 'ir, Ctx>(
entries: Vec<'ir, ast::Entry>, entries: Vec<'ir, ast::Entry>,
ctx: &mut Ctx, ctx: &mut Ctx,
) -> Result<GhostRoIrRef<'id, 'ir>> ) -> Result<IrRef<'id, 'ir>>
where where
Ctx: DowngradeContext<'id, 'ir>, Ctx: DowngradeContext<'id, 'ir>,
{ {
@@ -1169,18 +1077,14 @@ fn downgrade_rec_attrs_impl<'id, 'ir, Ctx, F, const ALLOW_DYN: bool>(
entries: Vec<'ir, ast::Entry>, entries: Vec<'ir, ast::Entry>,
ctx: &mut Ctx, ctx: &mut Ctx,
body_fn: F, body_fn: F,
) -> Result<GhostRoIrRef<'id, 'ir>> ) -> Result<IrRef<'id, 'ir>>
where where
Ctx: DowngradeContext<'id, 'ir>, Ctx: DowngradeContext<'id, 'ir>,
F: FnOnce( F: FnOnce(
&mut Ctx, &mut Ctx,
&[StringId], &[StringId],
&[( &[(IrRef<'id, 'ir>, IrRef<'id, 'ir>, TextRange)],
GhostRoIrRef<'id, 'ir>, ) -> Result<IrRef<'id, 'ir>>,
GhostRoMaybeThunkRef<'id, 'ir>,
TextRange,
)],
) -> Result<GhostRoIrRef<'id, 'ir>>,
{ {
let mut pending = PendingAttrSet::new_in(ctx.bump()); let mut pending = PendingAttrSet::new_in(ctx.bump());
pending.collect_entries(entries.iter().cloned(), ctx)?; pending.collect_entries(entries.iter().cloned(), ctx)?;
@@ -1192,21 +1096,18 @@ where
ctx.with_let_scope(&keys, |ctx| { ctx.with_let_scope(&keys, |ctx| {
let finalized = finalize_pending_set::<_, ALLOW_DYN>(pending, &inherit_lookups, ctx)?; let finalized = finalize_pending_set::<_, ALLOW_DYN>(pending, &inherit_lookups, ctx)?;
let vals = { let vals = keys
let mut temp = Vec::with_capacity_in(keys.len(), ctx.bump()); .iter()
for sym in &keys { .map(|sym| finalized.stcs.get(sym).expect("WTF").0)
temp.push(finalized.stcs.get(sym).expect("WTF").0); .collect_in(ctx.bump());
}
temp
};
body_fn(ctx, &keys, &finalized.dyns).map(|body| (vals, body)) body_fn(ctx, &keys, &finalized.dyns).map(|body| (vals, body))
}) })
} }
fn collect_inherit_lookups<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>>( fn collect_inherit_lookups<'id, 'ir, Ctx: DowngradeContext<'id, 'ir>>(
entries: &[ast::Entry], entries: &[ast::Entry],
ctx: &mut Ctx, ctx: &mut Ctx,
) -> Result<HashMap<'ir, StringId, (GhostRoMaybeThunkRef<'id, 'ir>, TextRange)>> { ) -> Result<HashMap<'ir, StringId, (IrRef<'id, 'ir>, TextRange)>> {
let mut inherit_lookups = HashMap::new_in(ctx.bump()); let mut inherit_lookups = HashMap::new_in(ctx.bump());
for entry in entries { for entry in entries {
if let ast::Entry::Inherit(inherit) = entry if let ast::Entry::Inherit(inherit) = entry
@@ -1215,7 +1116,7 @@ fn collect_inherit_lookups<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>>(
for attr in inherit.attrs() { for attr in inherit.attrs() {
if let ast::Attr::Ident(ident) = attr { if let ast::Attr::Ident(ident) = attr {
let attr_span = ident.syntax().text_range(); let attr_span = ident.syntax().text_range();
let sym = ctx.intern_string(ident.to_string()); let sym = ctx.new_sym(ident.to_string());
let expr = ctx.lookup(sym, attr_span)?; let expr = ctx.lookup(sym, attr_span)?;
inherit_lookups.insert(sym, (expr, attr_span)); inherit_lookups.insert(sym, (expr, attr_span));
} }
@@ -1234,7 +1135,7 @@ fn collect_binding_syms<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>, const AL
for (sym, (_, span)) in &pending.stcs { for (sym, (_, span)) in &pending.stcs {
if !binding_syms.insert(*sym) { if !binding_syms.insert(*sym) {
return Err(Error::downgrade_error( return Err(Error::downgrade_error(
format!("attribute '{}' already defined", ctx.resolve_sym(*sym)), format!("attribute '{}' already defined", ctx.get_sym(*sym)),
ctx.get_current_source(), ctx.get_current_source(),
*span, *span,
)); ));
@@ -1246,15 +1147,15 @@ fn collect_binding_syms<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>, const AL
fn finalize_pending_set<'id, 'ir, Ctx: DowngradeContext<'id, 'ir>, const ALLOW_DYN: bool>( fn finalize_pending_set<'id, 'ir, Ctx: DowngradeContext<'id, 'ir>, const ALLOW_DYN: bool>(
pending: PendingAttrSet, pending: PendingAttrSet,
inherit_lookups: &HashMap<StringId, (GhostRoMaybeThunkRef<'id, 'ir>, TextRange)>, inherit_lookups: &HashMap<StringId, (IrRef<'id, 'ir>, TextRange)>,
ctx: &mut Ctx, ctx: &mut Ctx,
) -> Result<FinalizedAttrSet<'id, 'ir>> { ) -> Result<FinalizedAttrSet<'id, 'ir>> {
let mut stcs = HashMap::new_in(ctx.bump()); let mut stcs = HashMap::new_in(ctx.bump());
let mut dyns = Vec::new_in(ctx.bump()); let mut dyns = Vec::new_in(ctx.bump());
for (sym, (value, value_span)) in pending.stcs { for (sym, (value, value_span)) in pending.stcs {
let expr = finalize_pending_value::<_, ALLOW_DYN>(value, inherit_lookups, ctx)?; let expr_id = finalize_pending_value::<_, ALLOW_DYN>(value, inherit_lookups, ctx)?;
stcs.insert(sym, (ctx.maybe_thunk(expr), value_span)); stcs.insert(sym, (expr_id, value_span));
} }
if ALLOW_DYN { if ALLOW_DYN {
@@ -1262,10 +1163,12 @@ fn finalize_pending_set<'id, 'ir, Ctx: DowngradeContext<'id, 'ir>, const ALLOW_D
let key_id = downgrade_attr(attr, ctx)?; let key_id = downgrade_attr(attr, ctx)?;
let key_expr = match key_id { let key_expr = match key_id {
Attr::Dynamic(id, _) => id, Attr::Dynamic(id, _) => id,
Attr::Str(sym, _attr_span) => ctx.new_expr(Ir::Str(sym)), Attr::Str(sym, _attr_span) => {
ctx.new_expr(Ir::Str(ctx.get_sym(sym).to_string().box_in(ctx.bump())))
}
}; };
let value = finalize_pending_value::<_, ALLOW_DYN>(value, inherit_lookups, ctx)?; let value_id = finalize_pending_value::<_, ALLOW_DYN>(value, inherit_lookups, ctx)?;
dyns.push((key_expr, ctx.maybe_thunk(value), value_span)); dyns.push((key_expr, value_id, value_span));
} }
} }
@@ -1274,26 +1177,30 @@ fn finalize_pending_set<'id, 'ir, Ctx: DowngradeContext<'id, 'ir>, const ALLOW_D
fn finalize_pending_value<'id, 'ir, Ctx: DowngradeContext<'id, 'ir>, const ALLOW_DYN: bool>( fn finalize_pending_value<'id, 'ir, Ctx: DowngradeContext<'id, 'ir>, const ALLOW_DYN: bool>(
value: PendingValue, value: PendingValue,
inherit_lookups: &HashMap<StringId, (GhostRoMaybeThunkRef<'id, 'ir>, TextRange)>, inherit_lookups: &HashMap<StringId, (IrRef<'id, 'ir>, TextRange)>,
ctx: &mut Ctx, ctx: &mut Ctx,
) -> Result<GhostRoIrRef<'id, 'ir>> { ) -> Result<IrRef<'id, 'ir>> {
match value { match value {
PendingValue::Expr(expr) => expr.downgrade(ctx), PendingValue::Expr(expr) => {
let id = Downgrade::downgrade(expr, ctx)?;
Ok(ctx.maybe_thunk(id))
}
PendingValue::InheritFrom(from_expr, sym, span) => { PendingValue::InheritFrom(from_expr, sym, span) => {
let from = Downgrade::downgrade(from_expr, ctx)?; let from_id = Downgrade::downgrade(from_expr, ctx)?;
Ok(ctx.new_expr(Ir::Select { let select_id = ctx.new_expr(Ir::Select {
expr: from, expr: from_id,
attrpath: Vec::from_iter_in([Attr::Str(sym, span)], ctx.bump()), attrpath: Vec::from_iter_in([Attr::Str(sym, span)], ctx.bump()),
default: None, default: None,
span, span,
})) });
Ok(ctx.maybe_thunk(select_id))
} }
PendingValue::InheritScope(sym, span) => { PendingValue::InheritScope(sym, span) => {
if let Some(&(expr, _)) = inherit_lookups.get(&sym) { if let Some(&(expr, _)) = inherit_lookups.get(&sym) {
Ok(ctx.new_expr(Ir::MaybeThunk(expr))) Ok(ctx.maybe_thunk(expr))
} else { } else {
ctx.lookup(sym, span) let lookup_id = ctx.lookup(sym, span)?;
.map(|val| ctx.new_expr(Ir::MaybeThunk(val))) Ok(ctx.maybe_thunk(lookup_id))
} }
} }
PendingValue::Set(set) => { PendingValue::Set(set) => {
@@ -1315,15 +1222,15 @@ fn finalize_pending_value<'id, 'ir, Ctx: DowngradeContext<'id, 'ir>, const ALLOW
fn downgrade_path<'id, 'ir>( fn downgrade_path<'id, 'ir>(
parts: impl IntoIterator<Item = ast::InterpolPart<ast::PathContent>>, parts: impl IntoIterator<Item = ast::InterpolPart<ast::PathContent>>,
_span: rnix::TextRange,
ctx: &mut impl DowngradeContext<'id, 'ir>, ctx: &mut impl DowngradeContext<'id, 'ir>,
) -> Result<GhostRoIrRef<'id, 'ir>> { ) -> Result<IrRef<'id, 'ir>> {
let bump = ctx.bump(); let bump = ctx.bump();
let parts = parts let parts = parts
.into_iter() .into_iter()
.map(|part| match part { .map(|part| match part {
ast::InterpolPart::Literal(lit) => { ast::InterpolPart::Literal(lit) => {
let id = ctx.intern_string(lit.text()); Ok(ctx.new_expr(Ir::Str(lit.text().to_string().box_in(ctx.bump()))))
Ok(ctx.new_expr(Ir::Str(id)))
} }
ast::InterpolPart::Interpolation(interpol) => interpol ast::InterpolPart::Interpolation(interpol) => interpol
.expr() .expr()
+693
View File
@@ -0,0 +1,693 @@
use std::hash::{Hash, Hasher};
use std::ops::Deref;
use bumpalo::Bump;
use bumpalo::boxed::Box;
use bumpalo::collections::Vec;
use gc_arena::Collect;
use ghost_cell::{GhostCell, GhostToken};
use rnix::{TextRange, ast};
use string_interner::symbol::SymbolU32;
use crate::runtime::BuiltinId;
pub type HashMap<'ir, K, V> = hashbrown::HashMap<K, V, hashbrown::DefaultHashBuilder, &'ir Bump>;
#[repr(transparent)]
#[derive(Clone, Copy)]
pub struct IrRef<'id, 'ir>(&'ir GhostCell<'id, Ir<'ir, Self>>);
impl<'id, 'ir> IrRef<'id, 'ir> {
pub fn new(ir: &'ir GhostCell<'id, Ir<'ir, Self>>) -> Self {
Self(ir)
}
pub fn alloc(bump: &'ir Bump, ir: Ir<'ir, Self>) -> Self {
Self(bump.alloc(GhostCell::new(ir)))
}
pub fn borrow<'a>(&'a self, token: &'a GhostToken<'id>) -> &'a Ir<'ir, Self> {
self.0.borrow(token)
}
/// Freeze a mutable IR reference into a read-only one, consuming the
/// `GhostToken` to prevent any further mutation.
///
/// # Safety
/// The transmute is sound because:
/// - `GhostCell<'id, T>` is `#[repr(transparent)]` over `T`
/// - `IrRef<'id, 'ir>` is `#[repr(transparent)]` over
/// `&'ir GhostCell<'id, Ir<'ir, Self>>`
/// - `RawIrRef<'ir>` is `#[repr(transparent)]` over `&'ir Ir<'ir, Self>`
/// - `Ir<'ir, Ref>` is `#[repr(C)]` and both ref types are pointer-sized
///
/// Consuming the `GhostToken` guarantees no `borrow_mut` calls can occur
/// afterwards, so the shared `&Ir` references from `RawIrRef::Deref` can
/// never alias with mutable references.
pub fn freeze(self, _token: GhostToken<'id>) -> RawIrRef<'ir> {
unsafe { std::mem::transmute(self) }
}
}
#[repr(transparent)]
#[derive(Clone, Copy, Debug)]
pub struct RawIrRef<'ir>(pub &'ir Ir<'ir, Self>);
impl<'ir> Deref for RawIrRef<'ir> {
type Target = Ir<'ir, RawIrRef<'ir>>;
fn deref(&self) -> &Self::Target {
self.0
}
}
#[repr(C)]
#[derive(Debug)]
pub enum Ir<'ir, Ref> {
Int(i64),
Float(f64),
Bool(bool),
Null,
Str(Box<'ir, String>),
AttrSet {
stcs: HashMap<'ir, StringId, (Ref, TextRange)>,
dyns: Vec<'ir, (Ref, Ref, TextRange)>,
},
List {
items: Vec<'ir, Ref>,
},
Path(Ref),
ConcatStrings {
parts: Vec<'ir, Ref>,
force_string: bool,
},
// OPs
UnOp {
rhs: Ref,
kind: UnOpKind,
},
BinOp {
lhs: Ref,
rhs: Ref,
kind: BinOpKind,
},
HasAttr {
lhs: Ref,
rhs: Vec<'ir, Attr<Ref>>,
},
Select {
expr: Ref,
attrpath: Vec<'ir, Attr<Ref>>,
default: Option<Ref>,
span: TextRange,
},
// Conditionals
If {
cond: Ref,
consq: Ref,
alter: Ref,
},
Assert {
assertion: Ref,
expr: Ref,
assertion_raw: String,
span: TextRange,
},
With {
namespace: Ref,
body: Ref,
thunks: Vec<'ir, (ThunkId, Ref)>,
},
WithLookup(StringId),
// Function related
Func {
body: Ref,
param: Option<Param<'ir>>,
arg: ArgId,
thunks: Vec<'ir, (ThunkId, Ref)>,
},
Arg(ArgId),
Call {
func: Ref,
arg: Ref,
span: TextRange,
},
// Builtins
Builtins,
Builtin(BuiltinId),
BuiltinConst(StringId),
// Misc
TopLevel {
body: Ref,
thunks: Vec<'ir, (ThunkId, Ref)>,
},
Thunk(ThunkId),
CurPos(TextRange),
ReplBinding(StringId),
ScopedImportBinding(StringId),
}
#[repr(transparent)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ThunkId(pub usize);
#[repr(transparent)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Collect)]
#[collect(require_static)]
pub struct StringId(pub SymbolU32);
#[repr(transparent)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ArgId(pub u32);
#[repr(transparent)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct SpanId(pub u32);
/// Represents a key in an attribute path.
#[allow(unused)]
#[derive(Debug)]
pub enum Attr<Ref> {
/// A dynamic attribute key, which is an expression that must evaluate to a string.
/// Example: `attrs.${key}`
Dynamic(Ref, TextRange),
/// A static attribute key.
/// Example: `attrs.key`
Str(StringId, TextRange),
}
/// The kinds of binary operations supported in Nix.
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
pub enum BinOpKind {
// Arithmetic
Add,
Sub,
Div,
Mul,
// Comparison
Eq,
Neq,
Lt,
Gt,
Leq,
Geq,
// Logical
And,
Or,
Impl,
// Set/String/Path operations
Con, // List concatenation (`++`)
Upd, // AttrSet update (`//`)
// Not standard, but part of rnix AST
PipeL,
PipeR,
}
impl From<ast::BinOpKind> for BinOpKind {
fn from(op: ast::BinOpKind) -> Self {
use BinOpKind::*;
use ast::BinOpKind as kind;
match op {
kind::Concat => Con,
kind::Update => Upd,
kind::Add => Add,
kind::Sub => Sub,
kind::Mul => Mul,
kind::Div => Div,
kind::And => And,
kind::Equal => Eq,
kind::Implication => Impl,
kind::Less => Lt,
kind::LessOrEq => Leq,
kind::More => Gt,
kind::MoreOrEq => Geq,
kind::NotEqual => Neq,
kind::Or => Or,
kind::PipeLeft => PipeL,
kind::PipeRight => PipeR,
}
}
}
/// The kinds of unary operations.
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
pub enum UnOpKind {
Neg, // Negation (`-`)
Not, // Logical not (`!`)
}
impl From<ast::UnaryOpKind> for UnOpKind {
fn from(value: ast::UnaryOpKind) -> Self {
match value {
ast::UnaryOpKind::Invert => UnOpKind::Not,
ast::UnaryOpKind::Negate => UnOpKind::Neg,
}
}
}
/// Describes the parameters of a function.
#[derive(Debug)]
pub struct Param<'ir> {
pub required: Vec<'ir, (StringId, TextRange)>,
pub optional: Vec<'ir, (StringId, TextRange)>,
pub ellipsis: bool,
}
#[derive(Clone, Copy)]
pub(crate) struct IrKey<'id, 'ir, 'a>(pub IrRef<'id, 'ir>, pub &'a GhostToken<'id>);
impl std::hash::Hash for IrKey<'_, '_, '_> {
fn hash<H: Hasher>(&self, state: &mut H) {
ir_content_hash(self.0, self.1, state);
}
}
impl PartialEq for IrKey<'_, '_, '_> {
fn eq(&self, other: &Self) -> bool {
ir_content_eq(self.0, other.0, self.1)
}
}
impl Eq for IrKey<'_, '_, '_> {}
fn attr_content_hash<'id>(
attr: &Attr<IrRef<'id, '_>>,
token: &GhostToken<'id>,
state: &mut impl Hasher,
) {
core::mem::discriminant(attr).hash(state);
match attr {
Attr::Dynamic(expr, _) => ir_content_hash(*expr, token, state),
Attr::Str(sym, _) => sym.hash(state),
}
}
fn attr_content_eq<'id, 'ir>(
a: &Attr<IrRef<'id, 'ir>>,
b: &Attr<IrRef<'id, 'ir>>,
token: &GhostToken<'id>,
) -> bool {
match (a, b) {
(Attr::Dynamic(ae, _), Attr::Dynamic(be, _)) => ir_content_eq(*ae, *be, token),
(Attr::Str(a, _), Attr::Str(b, _)) => a == b,
_ => false,
}
}
fn param_content_hash(param: &Param<'_>, state: &mut impl Hasher) {
param.required.len().hash(state);
for (sym, _) in param.required.iter() {
sym.hash(state);
}
param.optional.len().hash(state);
for (sym, _) in param.optional.iter() {
sym.hash(state);
}
param.ellipsis.hash(state);
}
fn param_content_eq(a: &Param<'_>, b: &Param<'_>) -> bool {
a.ellipsis == b.ellipsis
&& a.required.len() == b.required.len()
&& a.optional.len() == b.optional.len()
&& a.required
.iter()
.zip(b.required.iter())
.all(|((a, _), (b, _))| a == b)
&& a.optional
.iter()
.zip(b.optional.iter())
.all(|((a, _), (b, _))| a == b)
}
fn thunks_content_hash<'id>(
thunks: &[(ThunkId, IrRef<'id, '_>)],
token: &GhostToken<'id>,
state: &mut impl Hasher,
) {
thunks.len().hash(state);
for &(id, ir) in thunks {
id.hash(state);
ir_content_hash(ir, token, state);
}
}
fn thunks_content_eq<'id, 'ir>(
a: &[(ThunkId, IrRef<'id, 'ir>)],
b: &[(ThunkId, IrRef<'id, 'ir>)],
token: &GhostToken<'id>,
) -> bool {
a.len() == b.len()
&& a.iter()
.zip(b.iter())
.all(|(&(ai, ae), &(bi, be))| ai == bi && ir_content_eq(ae, be, token))
}
fn ir_content_hash<'id>(ir: IrRef<'id, '_>, token: &GhostToken<'id>, state: &mut impl Hasher) {
let ir = ir.borrow(token);
core::mem::discriminant(ir).hash(state);
match ir {
Ir::Int(x) => x.hash(state),
Ir::Float(x) => x.to_bits().hash(state),
Ir::Bool(x) => x.hash(state),
Ir::Null => {}
Ir::Str(x) => x.hash(state),
Ir::AttrSet { stcs, dyns } => {
stcs.len().hash(state);
let mut combined: u64 = 0;
for (&key, &(val, _)) in stcs.iter() {
let mut h = std::hash::DefaultHasher::new();
key.hash(&mut h);
ir_content_hash(val, token, &mut h);
combined = combined.wrapping_add(h.finish());
}
combined.hash(state);
dyns.len().hash(state);
for &(k, v, _) in dyns.iter() {
ir_content_hash(k, token, state);
ir_content_hash(v, token, state);
}
}
Ir::List { items } => {
items.len().hash(state);
for &item in items.iter() {
ir_content_hash(item, token, state);
}
}
Ir::HasAttr { lhs, rhs } => {
ir_content_hash(*lhs, token, state);
rhs.len().hash(state);
for attr in rhs.iter() {
attr_content_hash(attr, token, state);
}
}
&Ir::BinOp { lhs, rhs, kind } => {
ir_content_hash(lhs, token, state);
ir_content_hash(rhs, token, state);
kind.hash(state);
}
&Ir::UnOp { rhs, kind } => {
ir_content_hash(rhs, token, state);
kind.hash(state);
}
Ir::Select {
expr,
attrpath,
default,
..
} => {
ir_content_hash(*expr, token, state);
attrpath.len().hash(state);
for attr in attrpath.iter() {
attr_content_hash(attr, token, state);
}
default.is_some().hash(state);
if let Some(d) = default {
ir_content_hash(*d, token, state);
}
}
&Ir::If { cond, consq, alter } => {
ir_content_hash(cond, token, state);
ir_content_hash(consq, token, state);
ir_content_hash(alter, token, state);
}
&Ir::Call { func, arg, .. } => {
ir_content_hash(func, token, state);
ir_content_hash(arg, token, state);
}
Ir::Assert {
assertion,
expr,
assertion_raw,
..
} => {
ir_content_hash(*assertion, token, state);
ir_content_hash(*expr, token, state);
assertion_raw.hash(state);
}
Ir::ConcatStrings {
force_string,
parts,
} => {
force_string.hash(state);
parts.len().hash(state);
for &part in parts.iter() {
ir_content_hash(part, token, state);
}
}
&Ir::Path(expr) => ir_content_hash(expr, token, state),
Ir::Func {
body,
arg,
param,
thunks,
} => {
ir_content_hash(*body, token, state);
arg.hash(state);
param.is_some().hash(state);
if let Some(p) = param {
param_content_hash(p, state);
}
thunks_content_hash(thunks, token, state);
}
Ir::TopLevel { body, thunks } => {
ir_content_hash(*body, token, state);
thunks_content_hash(thunks, token, state);
}
Ir::Arg(x) => x.hash(state),
Ir::Thunk(x) => x.hash(state),
Ir::Builtins => {}
Ir::Builtin(x) => x.hash(state),
Ir::BuiltinConst(x) => x.hash(state),
Ir::CurPos(x) => x.hash(state),
Ir::ReplBinding(x) => x.hash(state),
Ir::ScopedImportBinding(x) => x.hash(state),
&Ir::With {
namespace,
body,
ref thunks,
} => {
ir_content_hash(namespace, token, state);
ir_content_hash(body, token, state);
thunks_content_hash(thunks, token, state);
}
Ir::WithLookup(x) => x.hash(state),
}
}
pub(crate) fn ir_content_eq<'id, 'ir>(
a: IrRef<'id, 'ir>,
b: IrRef<'id, 'ir>,
token: &GhostToken<'id>,
) -> bool {
std::ptr::eq(a.0, b.0)
|| match (a.borrow(token), b.borrow(token)) {
(Ir::Int(a), Ir::Int(b)) => a == b,
(Ir::Float(a), Ir::Float(b)) => a.to_bits() == b.to_bits(),
(Ir::Bool(a), Ir::Bool(b)) => a == b,
(Ir::Null, Ir::Null) => true,
(Ir::Str(a), Ir::Str(b)) => **a == **b,
(
Ir::AttrSet {
stcs: a_stcs,
dyns: a_dyns,
},
Ir::AttrSet {
stcs: b_stcs,
dyns: b_dyns,
},
) => {
a_stcs.len() == b_stcs.len()
&& a_dyns.len() == b_dyns.len()
&& a_stcs.iter().all(|(&k, &(av, _))| {
b_stcs
.get(&k)
.is_some_and(|&(bv, _)| ir_content_eq(av, bv, token))
})
&& a_dyns
.iter()
.zip(b_dyns.iter())
.all(|(&(ak, av, _), &(bk, bv, _))| {
ir_content_eq(ak, bk, token) && ir_content_eq(av, bv, token)
})
}
(Ir::List { items: a }, Ir::List { items: b }) => {
a.len() == b.len()
&& a.iter()
.zip(b.iter())
.all(|(&a, &b)| ir_content_eq(a, b, token))
}
(Ir::HasAttr { lhs: al, rhs: ar }, Ir::HasAttr { lhs: bl, rhs: br }) => {
ir_content_eq(*al, *bl, token)
&& ar.len() == br.len()
&& ar
.iter()
.zip(br.iter())
.all(|(a, b)| attr_content_eq(a, b, token))
}
(
&Ir::BinOp {
lhs: al,
rhs: ar,
kind: ak,
},
&Ir::BinOp {
lhs: bl,
rhs: br,
kind: bk,
},
) => ak == bk && ir_content_eq(al, bl, token) && ir_content_eq(ar, br, token),
(&Ir::UnOp { rhs: ar, kind: ak }, &Ir::UnOp { rhs: br, kind: bk }) => {
ak == bk && ir_content_eq(ar, br, token)
}
(
Ir::Select {
expr: ae,
attrpath: aa,
default: ad,
..
},
Ir::Select {
expr: be,
attrpath: ba,
default: bd,
..
},
) => {
ir_content_eq(*ae, *be, token)
&& aa.len() == ba.len()
&& aa
.iter()
.zip(ba.iter())
.all(|(a, b)| attr_content_eq(a, b, token))
&& match (ad, bd) {
(Some(a), Some(b)) => ir_content_eq(*a, *b, token),
(None, None) => true,
_ => false,
}
}
(
&Ir::If {
cond: ac,
consq: acs,
alter: aa,
},
&Ir::If {
cond: bc,
consq: bcs,
alter: ba,
},
) => {
ir_content_eq(ac, bc, token)
&& ir_content_eq(acs, bcs, token)
&& ir_content_eq(aa, ba, token)
}
(
&Ir::Call {
func: af, arg: aa, ..
},
&Ir::Call {
func: bf, arg: ba, ..
},
) => ir_content_eq(af, bf, token) && ir_content_eq(aa, ba, token),
(
Ir::Assert {
assertion: aa,
expr: ae,
assertion_raw: ar,
..
},
Ir::Assert {
assertion: ba,
expr: be,
assertion_raw: br,
..
},
) => ar == br && ir_content_eq(*aa, *ba, token) && ir_content_eq(*ae, *be, token),
(
Ir::ConcatStrings {
force_string: af,
parts: ap,
},
Ir::ConcatStrings {
force_string: bf,
parts: bp,
},
) => {
af == bf
&& ap.len() == bp.len()
&& ap
.iter()
.zip(bp.iter())
.all(|(&a, &b)| ir_content_eq(a, b, token))
}
(&Ir::Path(a), &Ir::Path(b)) => ir_content_eq(a, b, token),
(
Ir::Func {
body: ab,
arg: aa,
param: ap,
thunks: at,
},
Ir::Func {
body: bb,
arg: ba,
param: bp,
thunks: bt,
},
) => {
ir_content_eq(*ab, *bb, token)
&& aa == ba
&& match (ap, bp) {
(Some(a), Some(b)) => param_content_eq(a, b),
(None, None) => true,
_ => false,
}
&& thunks_content_eq(at, bt, token)
}
(
Ir::TopLevel {
body: ab,
thunks: at,
},
Ir::TopLevel {
body: bb,
thunks: bt,
},
) => ir_content_eq(*ab, *bb, token) && thunks_content_eq(at, bt, token),
(Ir::Arg(a), Ir::Arg(b)) => a == b,
(Ir::Thunk(a), Ir::Thunk(b)) => a == b,
(Ir::Builtins, Ir::Builtins) => true,
(Ir::Builtin(a), Ir::Builtin(b)) => a == b,
(Ir::CurPos(a), Ir::CurPos(b)) => a == b,
(Ir::ReplBinding(a), Ir::ReplBinding(b)) => a == b,
(Ir::ScopedImportBinding(a), Ir::ScopedImportBinding(b)) => a == b,
(
Ir::With {
namespace: a_ns,
body: a_body,
thunks: a_thunks,
},
Ir::With {
namespace: b_ns,
body: b_body,
thunks: b_thunks,
},
) => {
ir_content_eq(*a_ns, *b_ns, token)
&& ir_content_eq(*a_body, *b_body, token)
&& thunks_content_eq(a_thunks, b_thunks, token)
}
(Ir::WithLookup(a), Ir::WithLookup(b)) => a == b,
_ => false,
}
}
+17 -144
View File
@@ -1,149 +1,22 @@
use fix_bytecode::InstructionPtr; #![warn(clippy::unwrap_used)]
use fix_bytecode::disassembler::{Disassembler, DisassemblerContext}; #![allow(dead_code)]
use fix_compiler::{CodeState, ExtraScope};
use fix_error::{Result, Source};
use fix_lang::StringId;
use fix_runtime::{ForceMode, StaticValue, VmCode, VmContext, VmRuntimeCtx};
use fix_vm::Vm;
use hashbrown::{HashMap, HashSet};
use string_interner::{DefaultStringInterner, Symbol as _};
mod boxing;
pub mod error;
pub mod logging; pub mod logging;
pub mod runtime;
pub mod value;
mod codegen;
mod derivation;
mod disassembler;
mod downgrade;
// mod fetcher;
mod ir;
mod nar;
mod nix_utils;
mod store;
mod string_context;
#[global_allocator] #[global_allocator]
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
pub struct RuntimeState {
pub strings: DefaultStringInterner,
pub constants: Constants,
}
pub struct Evaluator {
pub runtime: RuntimeState,
pub code: CodeState,
}
impl Default for Evaluator {
fn default() -> Self {
Self::new()
}
}
impl Evaluator {
pub fn new() -> Self {
let mut strings = DefaultStringInterner::new();
let code = CodeState::new(&mut strings);
Self {
runtime: RuntimeState {
strings,
constants: Constants::default(),
},
code,
}
}
pub fn eval(&mut self, source: Source) -> Result<fix_lang::Value> {
self.do_eval(source, None, ForceMode::AsIs)
}
pub fn eval_shallow(&mut self, source: Source) -> Result<fix_lang::Value> {
self.do_eval(source, None, ForceMode::Shallow)
}
pub fn eval_deep(&mut self, source: Source) -> Result<fix_lang::Value> {
self.do_eval(source, None, ForceMode::Deep)
}
pub fn eval_repl(
&mut self,
source: Source,
scope: &HashSet<StringId>,
) -> Result<fix_lang::Value> {
self.do_eval(source, Some(ExtraScope::Repl(scope)), ForceMode::Shallow)
}
fn do_eval<'ctx>(
&'ctx mut self,
source: Source,
extra_scope: Option<ExtraScope<'ctx>>,
force_mode: ForceMode,
) -> Result<fix_lang::Value> {
let ip = self
.code
.compile_bytecode(source, extra_scope, &mut self.runtime)?;
Vm::run(self, ip, force_mode)
}
pub fn add_binding(
&mut self,
_ident: &str,
_expr: &str,
_scope: &mut HashSet<StringId>,
) -> Result<fix_lang::Value> {
todo!("add_binding")
}
pub fn compile_bytecode(&mut self, source: Source) -> Result<InstructionPtr> {
self.code.compile_bytecode(source, None, &mut self.runtime)
}
pub fn disassemble_colored(&self, ip: InstructionPtr) -> String {
Disassembler::new(ip, self).disassemble_colored()
}
}
impl VmRuntimeCtx for RuntimeState {
fn intern_string(&mut self, s: impl AsRef<str>) -> StringId {
StringId(self.strings.get_or_intern(s))
}
fn resolve_string(&self, id: StringId) -> &str {
self.strings
.resolve(id.0)
.expect("interned string id must resolve")
}
fn get_const(&self, id: u32) -> StaticValue {
self.constants.get(id).expect("const id must be valid")
}
fn add_const(&mut self, val: StaticValue) -> u32 {
self.constants.insert(val)
}
}
impl VmContext for Evaluator {
fn split(&mut self) -> (&mut impl VmCode, &mut impl VmRuntimeCtx) {
(&mut self.code, &mut self.runtime)
}
}
#[derive(Default)]
pub struct Constants {
data: Vec<StaticValue>,
dedup: HashMap<u64, u32>,
}
impl Constants {
fn insert(&mut self, val: StaticValue) -> u32 {
let bits = val.to_bits();
*self.dedup.entry(bits).or_insert_with(|| {
let idx = self.data.len() as u32;
self.data.push(val);
idx
})
}
fn get(&self, id: u32) -> Option<StaticValue> {
self.data.get(id as usize).copied()
}
}
impl DisassemblerContext for Evaluator {
fn get_code(&self) -> &[u8] {
&self.code.bytecode
}
fn resolve_string(&self, id: u32) -> &str {
let id = string_interner::symbol::SymbolU32::try_from_usize(id as usize)
.expect("invalid string id");
self.runtime.strings.resolve(id).expect("invalid string id")
}
}
+4 -3
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() -> Result<(), miette::InstallError> { pub fn init_logging() {
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() -> Result<(), miette::InstallError> {
.with(fmt_layer) .with(fmt_layer)
.init(); .init();
init_miette_handler() init_miette_handler();
} }
fn init_miette_handler() -> Result<(), miette::InstallError> { fn init_miette_handler() {
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,4 +46,5 @@ fn init_miette_handler() -> Result<(), miette::InstallError> {
.build(), .build(),
) )
})) }))
.ok();
} }
+33 -48
View File
@@ -3,8 +3,8 @@ use std::process::exit;
use anyhow::Result; use anyhow::Result;
use clap::{Args, Parser, Subcommand}; use clap::{Args, Parser, Subcommand};
use fix::Evaluator; use fix::error::Source;
use fix_error::Source; use fix::runtime::Runtime;
use hashbrown::HashSet; use hashbrown::HashSet;
use rustyline::DefaultEditor; use rustyline::DefaultEditor;
use rustyline::error::ReadlineError; use rustyline::error::ReadlineError;
@@ -20,54 +20,38 @@ struct Cli {
enum Command { enum Command {
Compile { Compile {
#[clap(flatten)] #[clap(flatten)]
source: ExprSourceArgs, source: ExprSource,
#[arg(long)] #[arg(long)]
silent: bool, silent: bool,
}, },
Eval { Eval {
#[clap(flatten)] #[clap(flatten)]
source: ExprSourceArgs, source: ExprSource,
}, },
Repl, Repl,
} }
#[derive(Args)] #[derive(Args)]
#[group(required = true, multiple = false)] #[group(required = true, multiple = false)]
struct ExprSourceArgs { struct ExprSource {
#[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 { fn run_compile(runtime: &mut Runtime, src: ExprSource, silent: bool) -> Result<()> {
Expr(String), let src = if let Some(expr) = src.expr {
File(PathBuf), Source::new_eval(expr)?
} } else if let Some(file) = src.file {
Source::new_file(file)?
#[expect( } else {
clippy::unreachable, 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<()> {
let src = match src {
ExprSource::Expr(expr) => Source::new_eval(expr)?,
ExprSource::File(file) => Source::new_file(file)?,
}; };
match eval.compile_bytecode(src) { match runtime.compile_bytecode(src) {
Ok(ip) => { Ok(ip) => {
if !silent { if !silent {
println!("{}", eval.disassemble_colored(ip)); println!("{}", runtime.disassemble_colored(ip));
} }
} }
Err(err) => { Err(err) => {
@@ -78,12 +62,15 @@ fn run_compile(eval: &mut Evaluator, src: ExprSource, silent: bool) -> Result<()
Ok(()) Ok(())
} }
fn run_eval(eval: &mut Evaluator, src: ExprSource) -> Result<()> { fn run_eval(runtime: &mut Runtime, src: ExprSource) -> Result<()> {
let src = match src { let src = if let Some(expr) = src.expr {
ExprSource::Expr(expr) => Source::new_eval(expr)?, Source::new_eval(expr)?
ExprSource::File(file) => Source::new_file(file)?, } else if let Some(file) = src.file {
Source::new_file(file)?
} else {
unreachable!()
}; };
match eval.eval_deep(src) { match runtime.eval_deep(src) {
Ok(value) => { Ok(value) => {
println!("{}", value.display_compat()); println!("{}", value.display_compat());
} }
@@ -95,20 +82,18 @@ fn run_eval(eval: &mut Evaluator, src: ExprSource) -> Result<()> {
Ok(()) Ok(())
} }
fn run_repl(eval: &mut Evaluator) -> Result<()> { fn run_repl(runtime: &mut Runtime) -> Result<()> {
let mut rl = DefaultEditor::new()?; let mut rl = DefaultEditor::new()?;
let mut scope = HashSet::new(); let mut scope = HashSet::new();
const RE: ere::Regex<3> = ere::compile_regex!("^[ \t]*([a-zA-Z_][a-zA-Z0-9_'-]*)[ \t]*(.*)$"); const RE: ere::Regex<3> = ere::compile_regex!("^[ \t]*([a-zA-Z_][a-zA-Z0-9_'-]*)[ \t]*(.*)$");
loop { loop {
let readline = rl.readline("fix-repl> "); let readline = rl.readline("nix-js-repl> ");
match readline { match readline {
Ok(line) => { Ok(line) => {
if line.trim().is_empty() { if line.trim().is_empty() {
continue; continue;
} }
if let Err(err) = rl.add_history_entry(line.as_str()) { let _ = 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();
@@ -116,20 +101,20 @@ fn run_repl(eval: &mut Evaluator) -> Result<()> {
eprintln!("Error: missing expression after '='"); eprintln!("Error: missing expression after '='");
continue; continue;
} }
match eval.add_binding(ident, expr, &mut scope) { match runtime.add_binding(ident, expr, &mut scope) {
Ok(value) => println!("{} = {}", ident, value), Ok(value) => println!("{} = {}", ident, value),
Err(err) => eprintln!("{:?}", miette::Report::new(*err)), Err(err) => eprintln!("{:?}", miette::Report::new(*err)),
} }
} else { } else {
let src = Source::new_repl(line)?; let src = Source::new_repl(line)?;
match eval.eval_repl(src, &scope) { match runtime.eval_repl(src, &scope) {
Ok(value) => println!("{value}"), Ok(value) => println!("{value}"),
Err(err) => eprintln!("{:?}", miette::Report::new(*err)), Err(err) => eprintln!("{:?}", miette::Report::new(*err)),
} }
} }
} else { } else {
let src = Source::new_repl(line)?; let src = Source::new_repl(line)?;
match eval.eval_repl(src, &scope) { match runtime.eval_repl(src, &scope) {
Ok(value) => println!("{value}"), Ok(value) => println!("{value}"),
Err(err) => eprintln!("{:?}", miette::Report::new(*err)), Err(err) => eprintln!("{:?}", miette::Report::new(*err)),
} }
@@ -152,15 +137,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 runtime = Runtime::new()?;
match cli.command { match cli.command {
Command::Compile { source, silent } => run_compile(&mut eval, source.into(), silent), Command::Compile { source, silent } => run_compile(&mut runtime, source, silent),
Command::Eval { source } => run_eval(&mut eval, source.into()), Command::Eval { source } => run_eval(&mut runtime, source),
Command::Repl => run_repl(&mut eval), Command::Repl => run_repl(&mut runtime),
} }
} }
+1 -1
View File
@@ -4,7 +4,7 @@ use std::path::Path;
use nix_nar::Encoder; use nix_nar::Encoder;
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
use fix_error::{Error, Result}; use crate::error::{Error, Result};
pub fn compute_nar_hash(path: &Path) -> Result<[u8; 32]> { pub fn compute_nar_hash(path: &Path) -> Result<[u8; 32]> {
let mut hasher = Sha256::new(); let mut hasher = Sha256::new();
+583
View File
@@ -0,0 +1,583 @@
use std::hash::BuildHasher;
use bumpalo::Bump;
use gc_arena::{Arena, Rootable};
use ghost_cell::{GhostCell, GhostToken};
use hashbrown::{DefaultHashBuilder, HashMap, HashSet, HashTable};
use rnix::TextRange;
use string_interner::symbol::SymbolU32;
use string_interner::{DefaultStringInterner, Symbol as _};
use crate::codegen::{BytecodeContext, InstructionPtr};
use crate::disassembler::{Disassembler, DisassemblerContext};
use crate::downgrade::{Downgrade as _, DowngradeContext};
use crate::error::{Error, Result, Source};
use crate::ir::{ArgId, Ir, IrKey, IrRef, RawIrRef, StringId, ThunkId, ir_content_eq};
use crate::runtime::builtins::init_builtins;
use crate::runtime::value::StaticValue;
use crate::runtime::vm::{ForceMode, new_gc_root};
use crate::store::{DaemonStore, StoreConfig};
use crate::value::Symbol;
mod builtins;
mod stack;
pub(crate) mod value;
mod vm;
pub(crate) use builtins::{BUILTINS, BuiltinId};
use stack::Stack;
use vm::{ErrorFrame, GcRoot};
pub struct Runtime {
// global
sources: Vec<Source>,
spans: Vec<(usize, TextRange)>,
store: DaemonStore,
strings: DefaultStringInterner,
// FIXME: remove?
thunk_count: usize,
bytecode: Vec<u8>,
pub(crate) constants: Vec<StaticValue>,
constant_dedup: HashMap<u64, u32>,
// downgrade
global_env: HashMap<StringId, Ir<'static, RawIrRef<'static>>>,
// eval
pc: usize,
fuel: usize,
error_contexts: Stack<8192, ErrorFrame>,
arena: Arena<Rootable![GcRoot<'_>]>,
force_mode: ForceMode,
}
impl Runtime {
const DEFAULT_FUEL_AMOUNT: usize = 2048;
pub fn new() -> Result<Self> {
// HACK: what the heck...
let mut global_env = HashMap::new();
let mut strings = DefaultStringInterner::new();
let arena = Arena::new(|mc| {
let (root, env) = new_gc_root(mc, &mut strings);
global_env = env;
root
});
let config = StoreConfig::from_env();
let store = DaemonStore::connect(&config.daemon_socket)?;
Ok(Self {
sources: Vec::new(),
spans: Vec::new(),
store,
strings,
thunk_count: 0,
bytecode: Vec::new(),
constants: Vec::new(),
constant_dedup: HashMap::new(),
global_env,
pc: 0,
fuel: Self::DEFAULT_FUEL_AMOUNT,
error_contexts: Stack::new(),
arena,
force_mode: ForceMode::default(),
})
}
pub fn eval(&mut self, source: Source) -> Result<crate::value::Value> {
self.do_eval(source, None, ForceMode::AsIs)
}
pub fn eval_shallow(&mut self, source: Source) -> Result<crate::value::Value> {
self.do_eval(source, None, ForceMode::Shallow)
}
pub fn eval_deep(&mut self, source: Source) -> Result<crate::value::Value> {
self.do_eval(source, None, ForceMode::Deep)
}
pub fn eval_repl(
&mut self,
source: Source,
scope: &HashSet<StringId>,
) -> Result<crate::value::Value> {
self.do_eval(source, Some(Scope::Repl(scope)), ForceMode::Shallow)
}
fn do_eval<'ctx>(
&'ctx mut self,
source: Source,
extra_scope: Option<Scope<'ctx>>,
force_mode: ForceMode,
) -> Result<crate::value::Value> {
let root = self.downgrade(source, extra_scope)?;
let ip = crate::codegen::compile_bytecode(root.as_ref(), self);
self.run(ip, force_mode)
}
pub fn add_binding(
&mut self,
_ident: &str,
_expr: &str,
_scope: &mut HashSet<StringId>,
) -> Result<crate::value::Value> {
todo!()
}
pub fn compile_bytecode(&mut self, source: Source) -> Result<InstructionPtr> {
let root = self.downgrade(source, None)?;
let ip = crate::codegen::compile_bytecode(root.as_ref(), self);
Ok(ip)
}
pub fn disassemble_colored(&mut self, ip: InstructionPtr) -> String {
Disassembler::new(ip, self).disassemble_colored()
}
pub fn disassemble(&mut self, ip: InstructionPtr) -> String {
Disassembler::new(ip, self).disassemble()
}
fn downgrade_ctx<'a, 'bump, 'id>(
&'a mut self,
bump: &'bump Bump,
token: GhostToken<'id>,
extra_scope: Option<Scope<'a>>,
) -> DowngradeCtx<'a, 'id, 'bump> {
let Runtime {
global_env,
sources,
thunk_count,
strings,
..
} = self;
DowngradeCtx {
bump,
token,
strings,
source: sources.last().expect("no current source").clone(),
scopes: [Scope::Global(global_env)]
.into_iter()
.chain(extra_scope)
.collect(),
with_scope_count: 0,
arg_count: 0,
thunk_count,
thunk_scopes: vec![ThunkScope::new_in(bump)],
}
}
fn downgrade<'a>(
&'a mut self,
source: Source,
extra_scope: Option<Scope<'a>>,
) -> Result<OwnedIr> {
tracing::debug!("Parsing Nix expression");
self.sources.push(source.clone());
let root = rnix::Root::parse(&source.src);
handle_parse_error(root.errors(), source).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 ir = self
.downgrade_ctx(&bump, token, extra_scope)
.downgrade_toplevel(expr)?;
let ir = unsafe { std::mem::transmute::<RawIrRef<'_>, RawIrRef<'static>>(ir) };
Ok(OwnedIr { _bump: bump, ir })
})
}
}
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> {
bump: &'ir Bump,
token: GhostToken<'id>,
strings: &'ctx mut DefaultStringInterner,
source: Source,
scopes: Vec<Scope<'ctx>>,
with_scope_count: u32,
arg_count: u32,
thunk_count: &'ctx mut usize,
thunk_scopes: Vec<ThunkScope<'id, 'ir>>,
}
fn should_thunk<'id>(ir: IrRef<'id, '_>, token: &GhostToken<'id>) -> bool {
!matches!(
ir.borrow(token),
Ir::Builtin(_)
| Ir::Builtins
| Ir::Int(_)
| Ir::Float(_)
| Ir::Bool(_)
| Ir::Null
| Ir::Str(_)
| Ir::Thunk(_)
)
}
impl<'ctx, 'id, 'ir> DowngradeCtx<'ctx, 'id, 'ir> {
fn new(
bump: &'ir Bump,
token: GhostToken<'id>,
symbols: &'ctx mut DefaultStringInterner,
global: &'ctx HashMap<StringId, Ir<'static, RawIrRef<'static>>>,
extra_scope: Option<Scope<'ctx>>,
thunk_count: &'ctx mut usize,
source: Source,
) -> Self {
Self {
bump,
token,
strings: symbols,
source,
scopes: std::iter::once(Scope::Global(global))
.chain(extra_scope)
.collect(),
thunk_count,
arg_count: 0,
with_scope_count: 0,
thunk_scopes: vec![ThunkScope::new_in(bump)],
}
}
}
impl<'ctx: 'ir, 'id, 'ir> DowngradeContext<'id, 'ir> for DowngradeCtx<'ctx, 'id, 'ir> {
fn new_expr(&self, expr: Ir<'ir, IrRef<'id, 'ir>>) -> IrRef<'id, 'ir> {
IrRef::new(self.bump.alloc(GhostCell::new(expr)))
}
fn new_arg(&mut self) -> ArgId {
self.arg_count += 1;
ArgId(self.arg_count - 1)
}
fn maybe_thunk(&mut self, ir: IrRef<'id, 'ir>) -> IrRef<'id, 'ir> {
if !should_thunk(ir, &self.token) {
return ir;
}
let cached = self
.thunk_scopes
.last()
.expect("no active cache scope")
.lookup_cache(ir, &self.token);
if let Some(id) = cached {
return IrRef::alloc(self.bump, Ir::Thunk(id));
}
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.token);
IrRef::alloc(self.bump, Ir::Thunk(id))
}
fn new_sym(&mut self, sym: String) -> StringId {
StringId(self.strings.get_or_intern(sym))
}
fn get_sym(&self, id: StringId) -> Symbol<'_> {
self.strings.resolve(id.0).expect("no symbol found").into()
}
fn lookup(&self, sym: StringId, span: TextRange) -> Result<IrRef<'id, 'ir>> {
for scope in self.scopes.iter().rev() {
match scope {
&Scope::Global(global_scope) => {
if let Some(expr) = global_scope.get(&sym) {
let ir = match expr {
Ir::Builtins => Ir::Builtins,
Ir::Builtin(s) => Ir::Builtin(*s),
Ir::Bool(b) => Ir::Bool(*b),
Ir::Null => Ir::Null,
_ => unreachable!("globals should only contain leaf IR nodes"),
};
return Ok(self.new_expr(ir));
}
}
&Scope::Repl(repl_bindings) => {
if repl_bindings.contains(&sym) {
return Ok(self.new_expr(Ir::ReplBinding(sym)));
}
}
Scope::ScopedImport(scoped_bindings) => {
if scoped_bindings.contains(&sym) {
return Ok(self.new_expr(Ir::ScopedImportBinding(sym)));
}
}
Scope::Let(let_scope) => {
if let Some(&expr) = let_scope.get(&sym) {
return Ok(self.new_expr(Ir::Thunk(expr)));
}
}
&Scope::Param(param_sym, id) => {
if param_sym == sym {
return Ok(self.new_expr(Ir::Arg(id)));
}
}
}
}
if self.with_scope_count > 0 {
Ok(self.new_expr(Ir::WithLookup(sym)))
} else {
Err(Error::downgrade_error(
format!("'{}' not found", self.get_sym(sym)),
self.get_current_source(),
span,
))
}
}
fn get_current_source(&self) -> Source {
self.source.clone()
}
fn with_let_scope<F, R>(&mut self, keys: &[StringId], f: F) -> Result<R>
where
F: FnOnce(&mut Self) -> Result<(bumpalo::collections::Vec<'ir, IrRef<'id, 'ir>>, R)>,
{
let base = *self.thunk_count;
*self.thunk_count = self
.thunk_count
.checked_add(keys.len())
.expect("thunk id overflow");
let iter = keys.iter().enumerate().map(|(offset, &key)| {
(
key,
ThunkId(unsafe { base.checked_add(offset).unwrap_unchecked() }),
)
});
self.scopes.push(Scope::Let(iter.collect()));
let (vals, ret) = {
let mut guard = ScopeGuard { ctx: self };
f(guard.as_ctx())?
};
assert_eq!(keys.len(), vals.len());
let scope = self.thunk_scopes.last_mut().expect("no active thunk scope");
scope.extend_bindings((base..base + keys.len()).map(ThunkId).zip(vals));
Ok(ret)
}
fn with_param_scope<F, R>(&mut self, param: StringId, arg: ArgId, f: F) -> R
where
F: FnOnce(&mut Self) -> R,
{
self.scopes.push(Scope::Param(param, arg));
let mut guard = ScopeGuard { ctx: self };
f(guard.as_ctx())
}
fn with_with_scope<F, R>(&mut self, f: F) -> R
where
F: FnOnce(&mut Self) -> R,
{
self.with_scope_count += 1;
let ret = f(self);
self.with_scope_count -= 1;
ret
}
fn with_thunk_scope<F, R>(
&mut self,
f: F,
) -> (
R,
bumpalo::collections::Vec<'ir, (ThunkId, IrRef<'id, 'ir>)>,
)
where
F: FnOnce(&mut Self) -> R,
{
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> DowngradeCtx<'ctx, 'id, 'ir> {
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;
let ir = IrRef::alloc(self.bump, Ir::TopLevel { body, thunks });
Ok(ir.freeze(self.token))
}
}
struct ThunkScope<'id, 'ir> {
bindings: bumpalo::collections::Vec<'ir, (ThunkId, IrRef<'id, 'ir>)>,
cache: HashTable<(IrRef<'id, 'ir>, ThunkId)>,
hasher: DefaultHashBuilder,
}
impl<'id, 'ir> ThunkScope<'id, 'ir> {
fn new_in(bump: &'ir Bump) -> Self {
Self {
bindings: bumpalo::collections::Vec::new_in(bump),
cache: HashTable::new(),
hasher: DefaultHashBuilder::default(),
}
}
fn lookup_cache(&self, key: IrRef<'id, 'ir>, token: &GhostToken<'id>) -> Option<ThunkId> {
let hash = self.hasher.hash_one(IrKey(key, token));
self.cache
.find(hash, |&(ir, _)| ir_content_eq(key, ir, token))
.map(|&(_, id)| id)
}
fn add_binding(&mut self, id: ThunkId, ir: IrRef<'id, 'ir>, token: &GhostToken<'id>) {
self.bindings.push((id, ir));
let hash = self.hasher.hash_one(IrKey(ir, token));
self.cache.insert_unique(hash, (ir, id), |&(ir, _)| {
self.hasher.hash_one(IrKey(ir, token))
});
}
fn extend_bindings(&mut self, iter: impl IntoIterator<Item = (ThunkId, IrRef<'id, 'ir>)>) {
self.bindings.extend(iter);
}
}
enum Scope<'ctx> {
Global(&'ctx HashMap<StringId, Ir<'static, RawIrRef<'static>>>),
Repl(&'ctx HashSet<StringId>),
ScopedImport(HashSet<StringId>),
Let(HashMap<StringId, ThunkId>),
Param(StringId, ArgId),
}
struct ScopeGuard<'a, 'ctx, 'id, 'ir> {
ctx: &'a mut DowngradeCtx<'ctx, 'id, 'ir>,
}
impl Drop for ScopeGuard<'_, '_, '_, '_> {
fn drop(&mut self) {
self.ctx.scopes.pop();
}
}
impl<'id, 'ir, 'ctx> ScopeGuard<'_, 'ctx, 'id, 'ir> {
fn as_ctx(&mut self) -> &mut DowngradeCtx<'ctx, 'id, 'ir> {
self.ctx
}
}
struct OwnedIr {
_bump: Bump,
ir: RawIrRef<'static>,
}
impl OwnedIr {
unsafe fn new(ir: RawIrRef<'_>, bump: Bump) -> Self {
Self {
_bump: bump,
ir: unsafe { std::mem::transmute::<RawIrRef<'_>, RawIrRef<'static>>(ir) },
}
}
fn as_ref(&self) -> RawIrRef<'_> {
self.ir
}
}
impl BytecodeContext for Runtime {
fn intern_string(&mut self, s: &str) -> StringId {
StringId(self.strings.get_or_intern(s))
}
fn register_span(&mut self, range: TextRange) -> u32 {
let id = self.spans.len();
let source_id = self
.sources
.len()
.checked_sub(1)
.expect("current_source not set");
self.spans.push((source_id, range));
id as u32
}
fn get_code(&self) -> &[u8] {
&self.bytecode
}
fn get_code_mut(&mut self) -> &mut Vec<u8> {
&mut self.bytecode
}
fn add_constant(&mut self, val: StaticValue) -> u32 {
let bits = val.to_bits();
*self.constant_dedup.entry(bits).or_insert_with(|| {
let idx = self.constants.len() as u32;
self.constants.push(val);
idx
})
}
}
impl DisassemblerContext for Runtime {
fn lookup_string(&self, id: u32) -> &str {
self.strings
.resolve(SymbolU32::try_from_usize(id as usize).expect("invalid string id"))
.expect("string not found")
}
fn get_code(&self) -> &[u8] {
&self.bytecode
}
}
struct WellKnownSymbols {
// TODO:
}
+410
View File
@@ -0,0 +1,410 @@
use gc_arena::{Collect, Gc, Mutation, RefLock};
use hashbrown::HashMap;
use num_enum::TryFromPrimitive;
use smallvec::SmallVec;
use string_interner::DefaultStringInterner;
use string_interner::symbol::SymbolU32;
use super::value::*;
use crate::ir::{Ir, RawIrRef, StringId};
/// Generates both the BUILTINS const table and the BuiltinId enum
/// from a single source of truth, preventing index desync.
macro_rules! define_builtins {
($(($name:literal, $variant:ident, $arity:expr)),* $(,)?) => {
/// Builtin function registry.
/// Array index IS the PrimOp id. (name, arity) pairs.
pub(crate) const BUILTINS: &[(&str, u8)] = &[
$(($name, $arity),)*
];
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, TryFromPrimitive, Collect)]
#[repr(u8)]
#[collect(require_static)]
pub(crate) 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),
("null", Null, 0), // constant, not a function
("__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),
("__unsafeGetAttrPos", UnsafeGetAttrPos, 2),
("__warn", Warn, 2),
("__zipAttrsWith", ZipAttrsWith, 2),
}
/// Names that need to be pre-interned for builtin implementations.
const EXTRA_INTERN_NAMES: &[&str] = &[
"builtins",
"currentSystem",
"langVersion",
"nixVersion",
"storeDir",
"nixPath",
"true",
"false",
// typeOf return values
"int",
"float",
"bool",
"string",
"path",
"null",
"set",
"list",
"lambda",
// attrset keys used by builtins
"name",
"value",
"success",
"right",
"wrong",
"key",
"operator",
"startSet",
"__toString",
"outPath",
"__functor",
"drvPath",
"type",
"derivation",
"version",
];
/// Returns true if this builtin has lazy argument semantics
/// (not all args should be forced before dispatch).
pub(super) fn is_lazy_builtin(id: BuiltinId) -> bool {
matches!(
id,
BuiltinId::Seq
| BuiltinId::DeepSeq
| BuiltinId::Trace
| BuiltinId::Warn
| BuiltinId::TryEval
| BuiltinId::AddErrorContext
| BuiltinId::Break
)
}
/// Intern all builtin names and extra names needed at runtime.
fn intern_all_builtins(interner: &mut DefaultStringInterner) {
for &(name, _) in BUILTINS {
interner.get_or_intern(name);
}
for &name in EXTRA_INTERN_NAMES {
interner.get_or_intern(name);
}
}
pub(super) fn init_builtins<'gc>(
mc: &Mutation<'gc>,
strings: &mut DefaultStringInterner,
) -> (
HashMap<StringId, Ir<'static, RawIrRef<'static>>>,
Value<'gc>,
) {
let mut global_env = HashMap::new();
let builtins_sym = StringId(strings.get_or_intern("builtins"));
global_env.insert(builtins_sym, Ir::Builtins);
let mut entries = SmallVec::with_capacity(BUILTINS.len());
for (idx, &(name, arity)) in BUILTINS.iter().enumerate() {
let id = BuiltinId::try_from_primitive(idx as u8).expect("infallible");
if let Some(local_name) = name.strip_prefix("__") {
// scoped builtins
let name = StringId(strings.get_or_intern(name));
let local_name = StringId(strings.get_or_intern(local_name));
global_env.insert(name, Ir::Builtin(id));
entries.push((local_name, Value::new_inline(PrimOp { id, arity })));
} else {
// global builtins
let name = StringId(strings.get_or_intern(name));
global_env.insert(name, Ir::Builtin(id));
entries.push((name, Value::new_inline(PrimOp { id, arity })));
}
}
let consts = [
(
"__currentSystem",
Ir::BuiltinConst(StringId(strings.get_or_intern("currentSystem"))),
// FIXME: detect currentSystem
Value::new_gc(Gc::new(mc, NixString::new("x86_64-linux"))),
),
("__langVersion", Ir::Int(6), Value::new_inline(6i32)),
(
"__nixVersion",
Ir::BuiltinConst(StringId(strings.get_or_intern("nixVersion"))),
Value::new_gc(Gc::new(mc, NixString::new("2.24.0"))),
),
(
"__storeDir",
Ir::BuiltinConst(StringId(strings.get_or_intern("storeDir"))),
Value::new_gc(Gc::new(mc, NixString::new("/nix/store"))),
),
(
"__nixPath",
Ir::BuiltinConst(StringId(strings.get_or_intern("nixPath"))),
// FIXME: get from config
Value::new_gc(Gc::new(
mc,
List {
inner: SmallVec::new(),
},
)),
),
("null", Ir::Null, Value::new_inline(Null)),
("true", Ir::Bool(true), Value::new_inline(true)),
("false", Ir::Bool(false), Value::new_inline(false)),
];
for (idx, &(name, arity)) in BUILTINS.iter().enumerate() {
let id = BuiltinId::try_from_primitive(idx as u8).expect("infallible");
if let Some(local_name) = name.strip_prefix("__") {
// scoped builtins
let name = StringId(strings.get_or_intern(name));
let local_name = StringId(strings.get_or_intern(local_name));
global_env.insert(name, Ir::Builtin(id));
entries.push((local_name, Value::new_inline(PrimOp { id, arity })));
} else {
// global builtins
let name = StringId(strings.get_or_intern(name));
global_env.insert(name, Ir::Builtin(id));
entries.push((name, Value::new_inline(PrimOp { id, arity })));
}
}
for (name, ir, val) in consts {
if let Some(local_name) = name.strip_prefix("__") {
let name = StringId(strings.get_or_intern(name));
let local_name = StringId(strings.get_or_intern(local_name));
global_env.insert(name, ir);
entries.push((local_name, val));
} else {
let name = StringId(strings.get_or_intern(name));
global_env.insert(name, ir);
entries.push((name, val));
}
}
// Self-reference thunk for builtins.builtins
let self_ref_thunk: Gc<'gc, Thunk<'gc>> = Gc::new(mc, RefLock::new(ThunkState::Blackhole));
let sym = strings.get_or_intern("builtins");
entries.push((StringId(sym), Value::new_gc(self_ref_thunk)));
entries.sort_by_key(|(k, _)| *k);
let builtins_set = Gc::new(mc, unsafe { AttrSet::from_sorted_unchecked(entries) });
let builtins_val = Value::new_gc(builtins_set);
(global_env, builtins_val)
}
pub(super) type PrimOpArgs<'gc> = [Value<'gc>; 3];
pub(super) type PrimOpStrictArgs<'gc> = [StrictValue<'gc>; 3];
#[allow(non_snake_case)]
pub(super) struct WellKnownSymbols {
abort: SymbolU32,
add: SymbolU32,
addErrorContext: SymbolU32,
all: SymbolU32,
any: SymbolU32,
appendContext: SymbolU32,
attrNames: SymbolU32,
attrValues: SymbolU32,
baseNameOf: SymbolU32,
bitAnd: SymbolU32,
bitOr: SymbolU32,
bitXor: SymbolU32,
catAttrs: SymbolU32,
ceil: SymbolU32,
compareVersions: SymbolU32,
concatLists: SymbolU32,
concatMap: SymbolU32,
concatStringsSep: SymbolU32,
convertHash: SymbolU32,
deepSeq: SymbolU32,
derivation: SymbolU32,
derivationStrict: SymbolU32,
dirOf: SymbolU32,
div: SymbolU32,
elem: SymbolU32,
elemAt: SymbolU32,
fetchGit: SymbolU32,
fetchMercurial: SymbolU32,
fetchTarball: SymbolU32,
fetchTree: SymbolU32,
fetchurl: SymbolU32,
filter: SymbolU32,
filterSource: SymbolU32,
findFile: SymbolU32,
floor: SymbolU32,
foldl: SymbolU32,
fromJSON: SymbolU32,
fromTOML: SymbolU32,
functionArgs: SymbolU32,
genList: SymbolU32,
genericClosure: SymbolU32,
getAttr: SymbolU32,
getContext: SymbolU32,
getEnv: SymbolU32,
groupBy: SymbolU32,
hasAttr: SymbolU32,
hasContext: SymbolU32,
hashFile: SymbolU32,
hashString: SymbolU32,
head: SymbolU32,
import: SymbolU32,
intersectAttrs: SymbolU32,
isAttrs: SymbolU32,
isBool: SymbolU32,
isFloat: SymbolU32,
isFunction: SymbolU32,
isInt: SymbolU32,
isList: SymbolU32,
isNull: SymbolU32,
isPath: SymbolU32,
isString: SymbolU32,
length: SymbolU32,
lessThan: SymbolU32,
listToAttrs: SymbolU32,
map: SymbolU32,
mapAttrs: SymbolU32,
match_: SymbolU32,
mul: SymbolU32,
null: SymbolU32,
parseDrvName: SymbolU32,
partition: SymbolU32,
path: SymbolU32,
pathExists: SymbolU32,
placeholder: SymbolU32,
readDir: SymbolU32,
readFile: SymbolU32,
readFileType: SymbolU32,
removeAttrs: SymbolU32,
replaceStrings: SymbolU32,
scopedImport: SymbolU32,
seq: SymbolU32,
sort: SymbolU32,
split: SymbolU32,
splitVersion: SymbolU32,
storePath: SymbolU32,
stringLength: SymbolU32,
sub: SymbolU32,
substring: SymbolU32,
tail: SymbolU32,
throw: SymbolU32,
toFile: SymbolU32,
toJSON: SymbolU32,
toPath: SymbolU32,
toString: SymbolU32,
toXML: SymbolU32,
trace: SymbolU32,
tryEval: SymbolU32,
typeOf: SymbolU32,
unsafeDiscardStringContext: SymbolU32,
unsafeGetAttrPos: SymbolU32,
warn: SymbolU32,
zipAttrsWith: SymbolU32,
break_: SymbolU32,
}
+31
View File
@@ -0,0 +1,31 @@
drvAttrs@{
outputs ? [ "out" ],
...
}:
let
strict = derivationStrict drvAttrs;
commonAttrs =
drvAttrs
// (builtins.listToAttrs outputsList)
// {
all = map (x: x.value) outputsList;
inherit drvAttrs;
};
outputToAttrListElement = outputName: {
name = outputName;
value = commonAttrs // {
outPath = builtins.getAttr outputName strict;
drvPath = strict.drvPath;
type = "derivation";
inherit outputName;
};
};
outputsList = map outputToAttrListElement outputs;
in
(builtins.head outputsList).value
+76
View File
@@ -0,0 +1,76 @@
{
system ? "", # obsolete
url,
hash ? "", # an SRI hash
# Legacy hash specification
md5 ? "",
sha1 ? "",
sha256 ? "",
sha512 ? "",
outputHash ?
if hash != "" then
hash
else if sha512 != "" then
sha512
else if sha1 != "" then
sha1
else if md5 != "" then
md5
else
sha256,
outputHashAlgo ?
if hash != "" then
""
else if sha512 != "" then
"sha512"
else if sha1 != "" then
"sha1"
else if md5 != "" then
"md5"
else
"sha256",
executable ? false,
unpack ? false,
name ? baseNameOf (toString url),
# still translates to __impure to trigger derivationStrict error checks.
impure ? false,
}:
derivation (
{
builder = "builtin:fetchurl";
# New-style output content requirements.
outputHashMode = if unpack || executable then "recursive" else "flat";
inherit
name
url
executable
unpack
;
system = "builtin";
# No need to double the amount of network traffic
preferLocalBuild = true;
impureEnvVars = [
# We borrow these environment variables from the caller to allow
# easy proxy configuration. This is impure, but a fixed-output
# derivation like fetchurl is allowed to do so since its result is
# by definition pure.
"http_proxy"
"https_proxy"
"ftp_proxy"
"all_proxy"
"no_proxy"
];
# To make "nix-prefetch-url" work.
urls = [ url ];
}
// (if impure then { __impure = true; } else { inherit outputHashAlgo outputHash; })
)
+88
View File
@@ -0,0 +1,88 @@
use std::mem::MaybeUninit;
use gc_arena::Collect;
use smallvec::SmallVec;
pub(super) struct Stack<const N: usize, T> {
inner: Box<[MaybeUninit<T>; N]>,
len: usize,
}
unsafe impl<'gc, const N: usize, T: Collect<'gc> + 'gc> Collect<'gc> for Stack<N, T> {
const NEEDS_TRACE: bool = T::NEEDS_TRACE;
fn trace<U: gc_arena::collect::Trace<'gc>>(&self, cc: &mut U) {
for item in self.inner[..self.len].iter() {
unsafe {
item.assume_init_ref().trace(cc);
}
}
}
}
impl<const N: usize, T> Default for Stack<N, T> {
fn default() -> Self {
Self::new()
}
}
impl<const N: usize, T> Stack<N, T> {
pub(super) fn new() -> Self {
Self {
inner: Box::new([const { MaybeUninit::uninit() }; N]),
len: 0,
}
}
pub(super) fn push(&mut self, val: T) -> Result<(), T> {
if self.len == N {
return Err(val);
}
unsafe {
self.inner.get_unchecked_mut(self.len).write(val);
}
self.len += 1;
Ok(())
}
pub(super) fn pop(&mut self) -> Option<T> {
if self.len == 0 {
return None;
}
let ret = unsafe {
self.inner
.get_unchecked_mut(self.len - 1)
.assume_init_read()
};
self.len -= 1;
Some(ret)
}
pub(super) fn tos(&self) -> Option<&T> {
if self.len == 0 {
return None;
}
Some(unsafe { self.inner.get_unchecked(self.len - 1).assume_init_ref() })
}
pub(super) fn tos_mut(&mut self) -> Option<&mut T> {
if self.len == 0 {
return None;
}
Some(unsafe { self.inner.get_unchecked_mut(self.len - 1).assume_init_mut() })
}
pub(super) fn len(&self) -> usize {
self.len
}
pub(super) fn pop_n<const M: usize>(&mut self, n: usize) -> SmallVec<[T; M]> {
assert!(n <= self.len, "pop_n: not enough items on stack");
let mut result = SmallVec::new();
let start = self.len - n;
for i in start..self.len {
result.push(unsafe { self.inner.get_unchecked(i).assume_init_read() });
}
self.len = start;
result
}
}
+603
View File
@@ -0,0 +1,603 @@
use std::fmt;
use std::marker::PhantomData;
use std::mem::size_of;
use std::ops::Deref;
use gc_arena::collect::Trace;
use gc_arena::{Collect, Gc, Mutation, RefLock};
use num_enum::TryFromPrimitive;
use sealed::sealed;
use smallvec::SmallVec;
use string_interner::Symbol;
use string_interner::symbol::SymbolU32;
use crate::boxing::{RawBox, RawStore, RawTag, Value as RawValue};
use crate::ir::StringId;
use crate::runtime::builtins::BuiltinId;
#[sealed]
/// # Safety
/// 1. TAG must be unique among all implementors.
/// 2. TAG must be within 1..=7
pub(crate) unsafe trait Storable {
const TAG: (bool, u8);
}
pub(crate) trait InlineStorable: Storable + RawStore {}
pub(crate) trait GcStorable: Storable {}
macro_rules! define_value_types {
(
inline { $($itype:ty => $itag:expr, $iname:literal;)* }
gc { $($gtype:ty => $gtag:expr, $gname:literal;)* }
) => {
$(
#[sealed]
unsafe impl Storable for $itype {
const TAG: (bool, u8) = $itag;
}
impl InlineStorable for $itype {}
)*
$(
#[sealed]
unsafe impl Storable for $gtype {
const TAG: (bool, u8) = $gtag;
}
impl GcStorable for $gtype {}
)*
const _: () = assert!(size_of::<Value<'static>>() == 8);
$(const _: () = assert!(size_of::<$itype>() <= 6);)*
$(const _: () = { let (_, val) = $itag; assert!(val >= 1 && val <= 7); };)*
$(const _: () = { let (_, val) = $gtag; assert!(val >= 1 && val <= 7); };)*
const _: () = {
let tags: &[(bool, u8)] = &[$($itag),*, $($gtag),*];
let mut mask_false: u8 = 0;
let mut mask_true: u8 = 0;
let mut i = 0;
while i < tags.len() {
let (neg, val) = tags[i];
let bit = 1 << val;
if neg {
assert!(mask_true & bit == 0, "duplicate true tag id");
mask_true |= bit;
} else {
assert!(mask_false & bit == 0, "duplicate false tag id");
mask_false |= bit;
}
i += 1;
}
};
unsafe impl<'gc> Collect<'gc> for Value<'gc> {
const NEEDS_TRACE: bool = true;
fn trace<T: Trace<'gc>>(&self, cc: &mut T) {
let Some(tag) = self.raw.tag() else { return };
match tag.neg_val() {
$(<$gtype as Storable>::TAG => unsafe {
self.load_gc::<$gtype>().trace(cc)
},)*
$(<$itype as Storable>::TAG => (),)*
(neg, val) => unreachable!("invalid tag: neg={neg}, val={val}"),
}
}
}
impl fmt::Debug for Value<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.tag() {
None => write!(f, "Float({:?})", unsafe {
self.raw.float().unwrap_unchecked()
}),
$(Some(<$itype as Storable>::TAG) => write!(f, "{}({:?})", $iname, unsafe {
self.as_inline::<$itype>().unwrap_unchecked()
}),)*
$(Some(<$gtype as Storable>::TAG) =>
write!(f, "{}(..)", $gname),)*
Some((neg, val)) => write!(f, "Unknown(neg={neg}, val={val})"),
}
}
}
impl fmt::Debug for StaticValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.tag() {
None => write!(f, "Float({:?})", unsafe {
self.raw.float().unwrap_unchecked()
}),
$(Some(<$itype as Storable>::TAG) => write!(f, "{}({:?})", $iname, unsafe {
self.as_inline::<$itype>().unwrap_unchecked()
}),)*
Some((neg, val)) => write!(f, "Unknown(neg={neg}, val={val})"),
}
}
}
};
}
define_value_types! {
inline {
i32 => (false, 1), "SmallInt";
bool => (false, 2), "Bool";
Null => (false, 3), "Null";
StringId => (false, 4), "SmallString";
PrimOp => (false, 5), "PrimOp";
}
gc {
i64 => (false, 6), "BigInt";
NixString => (false, 7), "String";
AttrSet<'_> => (true, 1), "AttrSet";
List<'_> => (true, 2), "List";
Thunk<'_> => (true, 3), "Thunk";
Closure<'_> => (true, 4), "Closure";
PrimOpApp<'_> => (true, 5), "PrimOpApp";
}
}
/// # Nix runtime value representation
///
/// NaN-boxed value fitting in 8 bytes.
#[derive(Copy, Clone)]
#[repr(transparent)]
pub(crate) struct Value<'gc> {
raw: RawBox,
_marker: PhantomData<Gc<'gc, ()>>,
}
impl Default for Value<'_> {
#[inline(always)]
fn default() -> Self {
Self::new_inline(Null)
}
}
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)
}
}
/// Returns the `(negative, val)` tag, or `None` for a float.
#[inline(always)]
fn tag(&self) -> Option<(bool, u8)> {
self.raw.tag().map(|t| t.neg_val())
}
}
impl<'gc> Value<'gc> {
#[inline]
pub(crate) fn new_float(val: f64) -> Self {
Self {
raw: RawBox::from_float(val),
_marker: PhantomData,
}
}
#[inline]
pub(crate) fn new_inline<T: InlineStorable>(val: T) -> Self {
Self::from_raw_value(RawValue::store(
unsafe { RawTag::new_unchecked(T::TAG.0, T::TAG.1) },
val,
))
}
#[inline]
pub(crate) fn new_gc<T: GcStorable>(gc: Gc<'gc, T>) -> Self {
let ptr = Gc::as_ptr(gc);
Self::from_raw_value(RawValue::store(
unsafe { RawTag::new_unchecked(T::TAG.0, T::TAG.1) },
ptr,
))
}
#[inline]
pub(crate) fn make_int(val: i64, mc: &Mutation<'gc>) -> Self {
if val >= i32::MIN as i64 && val <= i32::MAX as i64 {
Value::new_inline(val as i32)
} else {
Value::new_gc(Gc::new(mc, val))
}
}
}
impl<'gc> Value<'gc> {
#[inline]
pub(crate) fn is_float(&self) -> bool {
self.raw.is_float()
}
#[inline]
pub(crate) fn is<T: Storable>(&self) -> bool {
self.tag() == Some(T::TAG)
}
}
impl<'gc> Value<'gc> {
#[inline]
pub(crate) fn as_float(&self) -> Option<f64> {
self.raw.float().copied()
}
#[inline]
pub(crate) 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]
pub(crate) fn as_gc<T: GcStorable>(&self) -> Option<Gc<'gc, T>> {
if self.is::<T>() {
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(crate) fn restrict(self) -> Option<StrictValue<'gc>> {
if !self.is::<Thunk<'gc>>() {
Some(StrictValue(self))
} else {
None
}
}
}
#[derive(Copy, Clone)]
#[repr(transparent)]
pub(crate) struct StaticValue {
raw: RawBox,
}
impl Default for StaticValue {
#[inline(always)]
fn default() -> Self {
Self::new_inline(Null)
}
}
impl From<StaticValue> for Value<'_> {
#[inline]
fn from(value: StaticValue) -> Self {
Self {
raw: value.raw,
_marker: PhantomData,
}
}
}
impl StaticValue {
#[inline(always)]
fn from_raw_value(rv: RawValue) -> Self {
Self {
raw: RawBox::from_value(rv),
}
}
#[inline]
pub(crate) fn to_bits(self) -> u64 {
self.raw.to_bits()
}
#[inline]
pub(crate) fn new_float(val: f64) -> Self {
Self {
raw: RawBox::from_float(val),
}
}
#[inline]
pub(crate) fn new_inline<T: InlineStorable>(val: T) -> Self {
Self::from_raw_value(RawValue::store(
unsafe { RawTag::new_unchecked(T::TAG.0, T::TAG.1) },
val,
))
}
#[inline]
pub(crate) fn is_float(&self) -> bool {
self.raw.is_float()
}
#[inline]
pub(crate) fn is<T: InlineStorable>(&self) -> bool {
self.tag() == Some(T::TAG)
}
#[inline]
pub(crate) fn as_float(&self) -> Option<f64> {
self.raw.float().copied()
}
#[inline]
pub(crate) 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
}
}
/// Returns the `(negative, val)` tag, or `None` for a float.
#[inline(always)]
fn tag(&self) -> Option<(bool, u8)> {
self.raw.tag().map(|t| t.neg_val())
}
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct Null;
impl RawStore for Null {
fn to_val(self, value: &mut RawValue) {
value.set_data([0; 6]);
}
fn from_val(_: &RawValue) -> Self {
Self
}
}
impl RawStore for StringId {
fn to_val(self, value: &mut RawValue) {
(self.0.to_usize() as u32).to_val(value);
}
fn from_val(value: &RawValue) -> Self {
Self(
SymbolU32::try_from_usize(u32::from_val(value) as usize)
.expect("failed to read StringId from Value"),
)
}
}
/// Heap-allocated Nix string.
///
/// Stored on the GC heap via `Gc<'gc, NixString>`. The string data itself
/// lives in a standard `Box<str>` owned by this struct; the GC only manages
/// the outer allocation.
#[derive(Collect)]
#[collect(require_static)]
pub(crate) struct NixString {
data: Box<str>,
// TODO: string context for derivation dependency tracking
}
impl NixString {
pub(crate) fn new(s: impl Into<Box<str>>) -> Self {
Self { data: s.into() }
}
pub(crate) fn as_str(&self) -> &str {
&self.data
}
}
impl fmt::Debug for NixString {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Debug::fmt(&self.data, f)
}
}
#[derive(Collect, Debug, Default)]
#[collect(no_drop)]
pub(crate) struct AttrSet<'gc> {
entries: SmallVec<[(StringId, Value<'gc>); 4]>,
}
impl<'gc> Deref for AttrSet<'gc> {
type Target = [(StringId, Value<'gc>)];
fn deref(&self) -> &Self::Target {
&self.entries
}
}
impl<'gc> AttrSet<'gc> {
pub(crate) unsafe fn from_sorted_unchecked(
entries: SmallVec<[(StringId, Value<'gc>); 4]>,
) -> Self {
debug_assert!(entries.is_sorted_by_key(|(key, _)| *key));
Self { entries }
}
pub(crate) fn lookup(&self, key: StringId) -> Option<Value<'gc>> {
self.entries
.binary_search_by_key(&key, |(k, _)| *k)
.ok()
.map(|i| self.entries[i].1)
}
pub(crate) fn has(&self, key: StringId) -> bool {
self.entries.binary_search_by_key(&key, |(k, _)| *k).is_ok()
}
pub(crate) fn merge(&self, other: &Self, mc: &Mutation<'gc>) -> Gc<'gc, Self> {
use std::cmp::Ordering::*;
debug_assert!(self.entries.is_sorted_by_key(|(key, _)| *key));
debug_assert!(other.entries.is_sorted_by_key(|(key, _)| *key));
let mut entries = SmallVec::new();
let mut i = 0;
let mut j = 0;
while i < self.entries.len() && j < other.entries.len() {
match self.entries[i].0.cmp(&other.entries[j].0) {
Less => {
entries.push(self.entries[i]);
i += 1;
}
Greater => {
entries.push(other.entries[j]);
j += 1;
}
Equal => {
entries.push(other.entries[j]);
i += 1;
j += 1;
}
}
}
entries.extend(other.entries[j..].iter().cloned());
entries.extend(self.entries[i..].iter().cloned());
debug_assert!(entries.is_sorted_by_key(|(key, _)| *key));
Gc::new(mc, AttrSet { entries })
}
}
#[derive(Collect, Debug, Default)]
#[collect(no_drop)]
pub(crate) struct List<'gc> {
pub(crate) inner: SmallVec<[Value<'gc>; 4]>,
}
pub(crate) type Thunk<'gc> = RefLock<ThunkState<'gc>>;
#[derive(Collect, Debug)]
#[collect(no_drop)]
pub(crate) enum ThunkState<'gc> {
Pending {
ip: usize,
env: Gc<'gc, RefLock<Env<'gc>>>,
},
Apply {
func: Value<'gc>,
arg: Value<'gc>,
},
Blackhole,
Evaluated(Value<'gc>),
}
#[derive(Collect, Debug)]
#[collect(no_drop)]
pub(crate) struct Env<'gc> {
pub(crate) locals: SmallVec<[Value<'gc>; 4]>,
pub(crate) prev: Option<Gc<'gc, RefLock<Env<'gc>>>>,
}
impl<'gc> Env<'gc> {
pub(crate) fn empty() -> Self {
Env {
locals: SmallVec::new(),
prev: None,
}
}
pub(crate) 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];
locals[0] = arg;
Env {
locals,
prev: Some(prev),
}
}
}
#[derive(Collect, Debug)]
#[collect(no_drop)]
pub(crate) struct Closure<'gc> {
pub(crate) ip: u32,
pub(crate) n_locals: u32,
pub(crate) env: Gc<'gc, RefLock<Env<'gc>>>,
pub(crate) pattern: Option<Gc<'gc, PatternInfo>>,
}
#[derive(Collect, Debug)]
#[collect(require_static)]
pub(crate) struct PatternInfo {
pub(crate) required: SmallVec<[StringId; 4]>,
pub(crate) optional: SmallVec<[StringId; 4]>,
pub(crate) ellipsis: bool,
pub(crate) param_spans: Box<[(StringId, u32)]>,
}
#[derive(Clone, Copy, Debug, Collect)]
#[collect(require_static)]
pub(crate) struct PrimOp {
pub(crate) id: BuiltinId,
pub(crate) arity: u8,
}
impl RawStore for PrimOp {
fn to_val(self, value: &mut RawValue) {
value.set_data([0, 0, 0, 0, self.id as u8, self.arity]);
}
fn from_val(value: &RawValue) -> Self {
let [.., id, arity] = *value.data();
Self {
id: BuiltinId::try_from_primitive(id).expect("invalid BuiltinId"),
arity,
}
}
}
#[derive(Collect, Debug)]
#[collect(no_drop)]
pub(crate) struct PrimOpApp<'gc> {
pub(crate) primop: PrimOp,
pub(crate) args: SmallVec<[Value<'gc>; 2]>,
}
#[derive(Copy, Clone, Default)]
#[repr(transparent)]
pub(crate) struct StrictValue<'gc>(Value<'gc>);
impl<'gc> StrictValue<'gc> {
#[inline]
pub(crate) fn relax(self) -> Value<'gc> {
self.0
}
}
impl<'gc> Deref for StrictValue<'gc> {
type Target = Value<'gc>;
#[inline]
fn deref(&self) -> &Value<'gc> {
&self.0
}
}
impl fmt::Debug for StrictValue<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Debug::fmt(&self.0, f)
}
}
unsafe impl<'gc> Collect<'gc> for StrictValue<'gc> {
const NEEDS_TRACE: bool = true;
fn trace<T: gc_arena::collect::Trace<'gc>>(&self, cc: &mut T) {
self.0.trace(cc);
}
}
File diff suppressed because it is too large Load Diff
+209
View File
@@ -0,0 +1,209 @@
use std::collections::{BTreeMap, BTreeSet, VecDeque};
pub enum StringContextElem {
Opaque { path: String },
DrvDeep { drv_path: String },
Built { drv_path: String, output: String },
}
impl StringContextElem {
pub fn decode(encoded: &str) -> Self {
if let Some(drv_path) = encoded.strip_prefix('=') {
StringContextElem::DrvDeep {
drv_path: drv_path.to_string(),
}
} else if let Some(rest) = encoded.strip_prefix('!') {
if let Some(second_bang) = rest.find('!') {
let output = rest[..second_bang].to_string();
let drv_path = rest[second_bang + 1..].to_string();
StringContextElem::Built { drv_path, output }
} else {
StringContextElem::Opaque {
path: encoded.to_string(),
}
}
} else {
StringContextElem::Opaque {
path: encoded.to_string(),
}
}
}
}
pub type InputDrvs = BTreeMap<String, BTreeSet<String>>;
pub type Srcs = BTreeSet<String>;
pub fn extract_input_drvs_and_srcs(context: &[String]) -> Result<(InputDrvs, Srcs), String> {
let mut input_drvs: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
let mut input_srcs: BTreeSet<String> = BTreeSet::new();
for encoded in context {
match StringContextElem::decode(encoded) {
StringContextElem::Opaque { path } => {
input_srcs.insert(path);
}
StringContextElem::DrvDeep { drv_path } => {
compute_fs_closure(&drv_path, &mut input_drvs, &mut input_srcs)?;
}
StringContextElem::Built { drv_path, output } => {
input_drvs.entry(drv_path).or_default().insert(output);
}
}
}
Ok((input_drvs, input_srcs))
}
fn compute_fs_closure(
drv_path: &str,
input_drvs: &mut BTreeMap<String, BTreeSet<String>>,
input_srcs: &mut BTreeSet<String>,
) -> Result<(), String> {
let mut queue: VecDeque<String> = VecDeque::new();
let mut visited: BTreeSet<String> = BTreeSet::new();
queue.push_back(drv_path.to_string());
while let Some(current_path) = queue.pop_front() {
if visited.contains(&current_path) {
continue;
}
visited.insert(current_path.clone());
input_srcs.insert(current_path.clone());
if !current_path.ends_with(".drv") {
continue;
}
let content = std::fs::read_to_string(&current_path)
.map_err(|e| format!("failed to read derivation {}: {}", current_path, e))?;
let inputs = parse_derivation_inputs(&content)
.ok_or_else(|| format!("failed to parse derivation {}", current_path))?;
for src in inputs.input_srcs {
input_srcs.insert(src.clone());
if !visited.contains(&src) {
queue.push_back(src);
}
}
for (dep_drv, outputs) in inputs.input_drvs {
input_srcs.insert(dep_drv.clone());
let entry = input_drvs.entry(dep_drv.clone()).or_default();
for output in outputs {
entry.insert(output);
}
if !visited.contains(&dep_drv) {
queue.push_back(dep_drv);
}
}
}
Ok(())
}
struct DerivationInputs {
input_drvs: Vec<(String, Vec<String>)>,
input_srcs: Vec<String>,
}
fn parse_derivation_inputs(aterm: &str) -> Option<DerivationInputs> {
let aterm = aterm.strip_prefix("Derive([")?;
let mut bracket_count: i32 = 1;
let mut pos = 0;
let bytes = aterm.as_bytes();
while pos < bytes.len() && bracket_count > 0 {
match bytes[pos] {
b'[' => bracket_count += 1,
b']' => bracket_count -= 1,
_ => {}
}
pos += 1;
}
if bracket_count != 0 {
return None;
}
let rest = &aterm[pos..];
let rest = rest.strip_prefix(",[")?;
let mut input_drvs = Vec::new();
let mut bracket_count: i32 = 1;
let mut start = 0;
pos = 0;
let bytes = rest.as_bytes();
while pos < bytes.len() && bracket_count > 0 {
match bytes[pos] {
b'[' => bracket_count += 1,
b']' => bracket_count -= 1,
b'(' if bracket_count == 1 => {
start = pos;
}
b')' if bracket_count == 1 => {
let entry = &rest[start + 1..pos];
if let Some((drv_path, outputs)) = parse_input_drv_entry(entry) {
input_drvs.push((drv_path, outputs));
}
}
_ => {}
}
pos += 1;
}
let rest = &rest[pos..];
let rest = rest.strip_prefix(",[")?;
let mut input_srcs = Vec::new();
bracket_count = 1;
pos = 0;
let bytes = rest.as_bytes();
while pos < bytes.len() && bracket_count > 0 {
match bytes[pos] {
b'[' => bracket_count += 1,
b']' => bracket_count -= 1,
b'"' if bracket_count == 1 => {
pos += 1;
let src_start = pos;
while pos < bytes.len() && bytes[pos] != b'"' {
if bytes[pos] == b'\\' && pos + 1 < bytes.len() {
pos += 2;
} else {
pos += 1;
}
}
let src = std::str::from_utf8(&bytes[src_start..pos]).ok()?;
input_srcs.push(src.to_string());
}
_ => {}
}
pos += 1;
}
Some(DerivationInputs {
input_drvs,
input_srcs,
})
}
fn parse_input_drv_entry(entry: &str) -> Option<(String, Vec<String>)> {
let entry = entry.strip_prefix('"')?;
let quote_end = entry.find('"')?;
let drv_path = entry[..quote_end].to_string();
let rest = &entry[quote_end + 1..];
let rest = rest.strip_prefix(",[")?;
let rest = rest.strip_suffix(']')?;
let mut outputs = Vec::new();
for part in rest.split(',') {
let part = part.trim();
if let Some(name) = part.strip_prefix('"').and_then(|s| s.strip_suffix('"')) {
outputs.push(name.to_string());
}
}
Some((drv_path, outputs))
}
+36 -207
View File
@@ -1,173 +1,14 @@
use core::fmt::{Debug, Display, Formatter, Result as FmtResult};
use core::hash::Hash;
use core::ops::Deref;
use std::borrow::Cow; use std::borrow::Cow;
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::fmt::{Debug, Display, Formatter, Result as FmtResult}; use std::ops::DerefMut;
use std::ops::{Deref, DerefMut};
use gc_arena::Collect; use derive_more::{Constructor, IsVariant, Unwrap};
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)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Collect)]
#[collect(require_static)]
pub struct StringId(pub string_interner::symbol::SymbolU32);
/// Represents a Nix symbol, which is used as a key in attribute sets. /// Represents a Nix symbol, which is used as a key in attribute sets.
#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] #[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Constructor)]
pub struct Symbol<'a>(Cow<'a, str>); pub struct Symbol<'a>(Cow<'a, str>);
pub type StaticSymbol = Symbol<'static>; pub type StaticSymbol = Symbol<'static>;
@@ -219,36 +60,13 @@ impl Deref for Symbol<'_> {
} }
} }
fn escape_quote_string(s: &str) -> String {
let mut ret = String::with_capacity(s.len() + 2);
ret.push('"');
let mut iter = s.chars().peekable();
while let Some(c) = iter.next() {
match c {
'\\' => ret.push_str("\\\\"),
'"' => ret.push_str("\\\""),
'\n' => ret.push_str("\\n"),
'\r' => ret.push_str("\\r"),
'\t' => ret.push_str("\\t"),
'$' if iter.peek() == Some(&'{') => ret.push_str("\\$"),
c => ret.push(c),
}
}
ret.push('"');
ret
}
/// Represents a Nix attribute set, which is a map from symbols to values. /// Represents a Nix attribute set, which is a map from symbols to values.
#[derive(Default, Clone, PartialEq)] #[derive(Constructor, Default, Clone, PartialEq)]
pub struct AttrSet { pub struct AttrSet {
data: BTreeMap<StaticSymbol, Value>, data: BTreeMap<StaticSymbol, Value>,
} }
impl AttrSet { impl AttrSet {
pub fn new(data: BTreeMap<StaticSymbol, Value>) -> Self {
Self { data }
}
/// Gets a value by key (string or Symbol). /// Gets a value by key (string or Symbol).
pub fn get<'a, 'sym: 'a>(&'a self, key: impl Into<Symbol<'sym>>) -> Option<&'a Value> { pub fn get<'a, 'sym: 'a>(&'a self, key: impl Into<Symbol<'sym>>) -> Option<&'a Value> {
self.data.get(&key.into()) self.data.get(&key.into())
@@ -336,17 +154,11 @@ impl Display for AttrSetCompatDisplay<'_> {
} }
/// Represents a Nix list, which is a vector of values. /// Represents a Nix list, which is a vector of values.
#[derive(Default, Clone, Debug, PartialEq)] #[derive(Constructor, Default, Clone, Debug, PartialEq)]
pub struct List { pub struct List {
data: Vec<Value>, data: Vec<Value>,
} }
impl List {
pub fn new(data: Vec<Value>) -> Self {
Self { data }
}
}
impl Deref for List { impl Deref for List {
type Target = Vec<Value>; type Target = Vec<Value>;
fn deref(&self) -> &Self::Target { fn deref(&self) -> &Self::Target {
@@ -405,7 +217,7 @@ impl Display for ListCompatDisplay<'_> {
} }
/// Represents any possible Nix value that can be returned from an evaluation. /// Represents any possible Nix value that can be returned from an evaluation.
#[derive(Clone, Debug, PartialEq)] #[derive(IsVariant, Unwrap, Clone, Debug, PartialEq)]
pub enum Value { pub enum Value {
/// An integer value. /// An integer value.
Int(i64), Int(i64),
@@ -428,16 +240,35 @@ pub enum Value {
/// A function (lambda). /// A function (lambda).
Func, Func,
/// A primitive (built-in) operation. /// A primitive (built-in) operation.
PrimOp(&'static str), PrimOp(String),
/// A partially applied primitive operation. /// A partially applied primitive operation.
PrimOpApp(&'static str), PrimOpApp(String),
/// A marker for a value that has been seen before during serialization, to break cycles. /// A marker for a value that has been seen before during serialization, to break cycles.
/// This is used to prevent infinite recursion when printing or serializing cyclic data structures. /// This is used to prevent infinite recursion when printing or serializing cyclic data structures.
Repeated, Repeated,
} }
fn escape_quote_string(s: &str) -> String {
let mut ret = String::with_capacity(s.len() + 2);
ret.push('"');
let mut iter = s.chars().peekable();
while let Some(c) = iter.next() {
match c {
'\\' => ret.push_str("\\\\"),
'"' => ret.push_str("\\\""),
'\n' => ret.push_str("\\n"),
'\r' => ret.push_str("\\r"),
'\t' => ret.push_str("\\t"),
'$' if iter.peek() == Some(&'{') => ret.push_str("\\$"),
c => ret.push(c),
}
}
ret.push('"');
ret
}
/// Wrapper to format a float in Nix style (C printf `%g` with precision 6). /// Wrapper to format a float in Nix style (C printf `%g` with precision 6).
pub struct NixFloat(pub f64); pub(crate) struct NixFloat(pub f64);
impl Display for NixFloat { impl Display for NixFloat {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
@@ -461,10 +292,6 @@ 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$}")
@@ -489,9 +316,11 @@ fn fmt_nix_float(f: &mut Formatter<'_>, x: f64) -> FmtResult {
}; };
if formatted.contains('.') { if formatted.contains('.') {
if let Some((head, tail)) = formatted.split_once('e') { if let Some(e_pos) = formatted.find('e') {
let trimmed = head.trim_end_matches('0').trim_end_matches('.'); let trimmed = formatted[..e_pos]
write!(f, "{trimmed}e{tail}") .trim_end_matches('0')
.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}")
+7 -5
View File
@@ -1,4 +1,4 @@
use fix_lang::Value; use fix::value::Value;
use crate::utils::{eval_deep, eval_deep_result}; use crate::utils::{eval_deep, eval_deep_result};
@@ -402,15 +402,17 @@ fn fixed_output_sha256_flat() {
#[test_log::test] #[test_log::test]
fn fixed_output_missing_hashalgo() { fn fixed_output_missing_hashalgo() {
eval_deep_result( assert!(
r#"derivation { eval_deep_result(
r#"derivation {
name = "default"; name = "default";
builder = "/bin/sh"; builder = "/bin/sh";
system = "x86_64-linux"; system = "x86_64-linux";
outputHash = "0000000000000000000000000000000000000000000000000000000000000000"; outputHash = "0000000000000000000000000000000000000000000000000000000000000000";
}"#, }"#,
) )
.unwrap_err(); .is_err()
);
} }
#[test_log::test] #[test_log::test]
+7 -7
View File
@@ -1,6 +1,6 @@
use fix::Evaluator; use fix::error::Source;
use fix_error::Source; use fix::runtime::Runtime;
use fix_lang::Value; use fix::value::Value;
use crate::utils::{eval, eval_result}; use crate::utils::{eval, eval_result};
@@ -97,7 +97,7 @@ fn import_with_complex_dependency_graph() {
#[test_log::test] #[test_log::test]
fn path_with_file() { fn path_with_file() {
let mut ctx = Evaluator::new(); let mut ctx = Runtime::new().unwrap();
let temp_dir = tempfile::tempdir().unwrap(); let temp_dir = tempfile::tempdir().unwrap();
let test_file = temp_dir.path().join("test.txt"); let test_file = temp_dir.path().join("test.txt");
std::fs::write(&test_file, "Hello, World!").unwrap(); std::fs::write(&test_file, "Hello, World!").unwrap();
@@ -136,7 +136,7 @@ fn path_with_custom_name() {
#[test_log::test] #[test_log::test]
fn path_with_directory_recursive() { fn path_with_directory_recursive() {
let mut ctx = Evaluator::new(); let mut ctx = Runtime::new().unwrap();
let temp_dir = tempfile::tempdir().unwrap(); let temp_dir = tempfile::tempdir().unwrap();
let test_dir = temp_dir.path().join("mydir"); let test_dir = temp_dir.path().join("mydir");
std::fs::create_dir_all(&test_dir).unwrap(); std::fs::create_dir_all(&test_dir).unwrap();
@@ -159,7 +159,7 @@ fn path_with_directory_recursive() {
#[test_log::test] #[test_log::test]
fn path_flat_with_file() { fn path_flat_with_file() {
let mut ctx = Evaluator::new(); let mut ctx = Runtime::new().unwrap();
let temp_dir = tempfile::tempdir().unwrap(); let temp_dir = tempfile::tempdir().unwrap();
let test_file = temp_dir.path().join("flat.txt"); let test_file = temp_dir.path().join("flat.txt");
std::fs::write(&test_file, "Flat content").unwrap(); std::fs::write(&test_file, "Flat content").unwrap();
@@ -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);
result.unwrap_err(); assert!(result.is_err());
} }
#[test_log::test] #[test_log::test]
+231 -250
View File
@@ -2,10 +2,9 @@
use std::path::PathBuf; use std::path::PathBuf;
use fix::Evaluator; use fix::error::Source;
use fix_error::{Source, SourceType}; use fix::runtime::Runtime;
use fix_lang::Value; use fix::value::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")
@@ -15,10 +14,10 @@ fn eval_file(name: &str) -> Result<(Value, Source), String> {
let lang_dir = get_lang_dir(); let lang_dir = get_lang_dir();
let nix_path = lang_dir.join(format!("{name}.nix")); let nix_path = lang_dir.join(format!("{name}.nix"));
let mut ctx = Evaluator::new(); let mut ctx = Runtime::new().map_err(|e| e.to_string())?;
let content = std::fs::read_to_string(&nix_path).unwrap(); let content = std::fs::read_to_string(&nix_path).unwrap();
let source = Source { let source = Source {
ty: SourceType::File(nix_path.into()), ty: fix::error::SourceType::File(nix_path.into()),
src: content.into(), src: content.into(),
}; };
ctx.eval_deep(source.clone()) ctx.eval_deep(source.clone())
@@ -90,248 +89,230 @@ macro_rules! eval_fail_test {
}; };
} }
mod okay { eval_okay_test!(any_all);
use super::*; eval_okay_test!(arithmetic);
eval_okay_test!(attrnames);
eval_okay_test!(attrs);
eval_okay_test!(attrs2);
eval_okay_test!(attrs3);
eval_okay_test!(attrs4);
eval_okay_test!(attrs5);
eval_okay_test!(
#[ignore = "__overrides is not supported"]
attrs6
);
eval_okay_test!(
#[ignore = "requires --arg/--argstr CLI flags"]
autoargs
);
eval_okay_test!(backslash_newline_1);
eval_okay_test!(backslash_newline_2);
eval_okay_test!(baseNameOf);
eval_okay_test!(builtins);
eval_okay_test!(builtins_add);
eval_okay_test!(callable_attrs);
eval_okay_test!(catattrs);
eval_okay_test!(closure);
eval_okay_test!(comments);
eval_okay_test!(concat);
eval_okay_test!(concatmap);
eval_okay_test!(concatstringssep);
eval_okay_test!(context);
eval_okay_test!(context_introspection);
eval_okay_test!(convertHash);
eval_okay_test!(curpos);
eval_okay_test!(deepseq);
eval_okay_test!(delayed_with);
eval_okay_test!(delayed_with_inherit);
eval_okay_test!(deprecate_cursed_or);
eval_okay_test!(derivation_legacy);
eval_okay_test!(dynamic_attrs);
eval_okay_test!(dynamic_attrs_2);
eval_okay_test!(dynamic_attrs_bare);
eval_okay_test!(elem);
eval_okay_test!(empty_args);
eval_okay_test!(eq);
eval_okay_test!(eq_derivations);
eval_okay_test!(filter);
eval_okay_test!(
#[ignore = "not implemented: flakeRefToString"]
flake_ref_to_string
);
eval_okay_test!(flatten);
eval_okay_test!(float);
eval_okay_test!(floor_ceil);
eval_okay_test!(foldlStrict);
eval_okay_test!(foldlStrict_lazy_elements);
eval_okay_test!(foldlStrict_lazy_initial_accumulator);
eval_okay_test!(fromjson);
eval_okay_test!(fromjson_escapes);
eval_okay_test!(fromTOML);
eval_okay_test!(
#[ignore = "timestamps are not supported"]
fromTOML_timestamps
);
eval_okay_test!(functionargs);
eval_okay_test!(hashfile);
eval_okay_test!(hashstring);
eval_okay_test!(getattrpos);
eval_okay_test!(getattrpos_functionargs);
eval_okay_test!(getattrpos_undefined);
eval_okay_test!(getenv, || {
unsafe { std::env::set_var("TEST_VAR", "foo") };
});
eval_okay_test!(groupBy);
eval_okay_test!(r#if);
eval_okay_test!(ind_string);
eval_okay_test!(import);
eval_okay_test!(inherit_attr_pos);
eval_okay_test!(
#[ignore = "__overrides is not supported"]
inherit_from
);
eval_okay_test!(intersectAttrs);
eval_okay_test!(r#let);
eval_okay_test!(list);
eval_okay_test!(listtoattrs);
eval_okay_test!(logic);
eval_okay_test!(map);
eval_okay_test!(mapattrs);
eval_okay_test!(merge_dynamic_attrs);
eval_okay_test!(nested_with);
eval_okay_test!(new_let);
eval_okay_test!(null_dynamic_attrs);
eval_okay_test!(
#[ignore = "__overrides is not supported"]
overrides
);
eval_okay_test!(
#[ignore = "not implemented: parseFlakeRef"]
parse_flake_ref
);
eval_okay_test!(partition);
eval_okay_test!(path);
eval_okay_test!(pathexists);
eval_okay_test!(path_string_interpolation, || {
unsafe {
std::env::set_var("HOME", "/fake-home");
}
});
eval_okay_test!(patterns);
eval_okay_test!(print);
eval_okay_test!(readDir);
eval_okay_test!(readfile);
eval_okay_test!(readFileType);
eval_okay_test!(redefine_builtin);
eval_okay_test!(regex_match);
eval_okay_test!(regex_split);
eval_okay_test!(regression_20220122);
eval_okay_test!(regression_20220125);
eval_okay_test!(regrettable_rec_attrset_merge);
eval_okay_test!(remove);
eval_okay_test!(repeated_empty_attrs);
eval_okay_test!(repeated_empty_list);
eval_okay_test!(replacestrings);
eval_okay_test!(
#[ignore = "requires -I CLI flags"]
search_path
);
eval_okay_test!(scope_1);
eval_okay_test!(scope_2);
eval_okay_test!(scope_3);
eval_okay_test!(scope_4);
eval_okay_test!(scope_6);
eval_okay_test!(scope_7);
eval_okay_test!(seq);
eval_okay_test!(sort);
eval_okay_test!(splitversion);
eval_okay_test!(string);
eval_okay_test!(strings_as_attrs_names);
eval_okay_test!(substring);
eval_okay_test!(substring_context);
eval_okay_test!(symlink_resolution);
eval_okay_test!(
#[ignore = "TCO not implemented, also disabled in CppNix"]
tail_call_1
);
eval_okay_test!(tojson);
eval_okay_test!(toxml);
eval_okay_test!(toxml2);
eval_okay_test!(tryeval);
eval_okay_test!(types);
eval_okay_test!(versions);
eval_okay_test!(with);
eval_okay_test!(zipAttrsWith);
eval_okay_test!(any_all); eval_fail_test!(fail_abort);
eval_okay_test!(arithmetic); eval_fail_test!(fail_addDrvOutputDependencies_empty_context);
eval_okay_test!(attrnames); eval_fail_test!(fail_addDrvOutputDependencies_multi_elem_context);
eval_okay_test!(attrs); eval_fail_test!(fail_addDrvOutputDependencies_wrong_element_kind);
eval_okay_test!(attrs2); eval_fail_test!(fail_addErrorRuntime_example);
eval_okay_test!(attrs3); eval_fail_test!(fail_assert);
eval_okay_test!(attrs4); eval_fail_test!(fail_assert_equal_attrs_names);
eval_okay_test!(attrs5); eval_fail_test!(fail_assert_equal_attrs_names_2);
eval_okay_test!( eval_fail_test!(fail_assert_equal_derivations);
#[ignore = "__overrides is not supported"] eval_fail_test!(fail_assert_equal_derivations_extra);
attrs6 eval_fail_test!(fail_assert_equal_floats);
); eval_fail_test!(fail_assert_equal_function_direct);
eval_okay_test!( eval_fail_test!(fail_assert_equal_int_float);
#[ignore = "requires --arg/--argstr CLI flags"] eval_fail_test!(fail_assert_equal_ints);
autoargs eval_fail_test!(fail_assert_equal_list_length);
); eval_fail_test!(fail_assert_equal_paths);
eval_okay_test!(backslash_newline_1); eval_fail_test!(fail_assert_equal_type);
eval_okay_test!(backslash_newline_2); eval_fail_test!(fail_assert_equal_type_nested);
eval_okay_test!(baseNameOf); eval_fail_test!(fail_assert_nested_bool);
eval_okay_test!(builtins); eval_fail_test!(fail_attr_name_type);
eval_okay_test!(builtins_add); eval_fail_test!(fail_attrset_merge_drops_later_rec);
eval_okay_test!(callable_attrs); eval_fail_test!(fail_bad_string_interpolation_1);
eval_okay_test!(catattrs); eval_fail_test!(fail_bad_string_interpolation_2);
eval_okay_test!(closure); eval_fail_test!(fail_bad_string_interpolation_3);
eval_okay_test!(comments); eval_fail_test!(fail_bad_string_interpolation_4);
eval_okay_test!(concat); eval_fail_test!(fail_blackhole);
eval_okay_test!(concatmap); eval_fail_test!(fail_call_primop);
eval_okay_test!(concatstringssep); eval_fail_test!(fail_deepseq);
eval_okay_test!(context); eval_fail_test!(fail_derivation_name);
eval_okay_test!(context_introspection); eval_fail_test!(fail_dup_dynamic_attrs);
eval_okay_test!(convertHash); eval_fail_test!(fail_duplicate_traces);
eval_okay_test!(curpos); eval_fail_test!(fail_eol_1);
eval_okay_test!(deepseq); eval_fail_test!(fail_eol_2);
eval_okay_test!(delayed_with); eval_fail_test!(fail_eol_3);
eval_okay_test!(delayed_with_inherit); eval_fail_test!(fail_fetchTree_negative);
eval_okay_test!(deprecate_cursed_or); eval_fail_test!(fail_fetchurl_baseName);
eval_okay_test!(derivation_legacy); eval_fail_test!(fail_fetchurl_baseName_attrs);
eval_okay_test!(dynamic_attrs); eval_fail_test!(fail_fetchurl_baseName_attrs_name);
eval_okay_test!(dynamic_attrs_2); eval_fail_test!(fail_flake_ref_to_string_negative_integer);
eval_okay_test!(dynamic_attrs_bare); eval_fail_test!(fail_foldlStrict_strict_op_application);
eval_okay_test!(elem); eval_fail_test!(fail_fromJSON_keyWithNullByte);
eval_okay_test!(empty_args); eval_fail_test!(fail_fromJSON_overflowing);
eval_okay_test!(eq); eval_fail_test!(fail_fromJSON_valueWithNullByte);
eval_okay_test!(eq_derivations); eval_fail_test!(fail_fromTOML_keyWithNullByte);
eval_okay_test!(filter); eval_fail_test!(fail_fromTOML_timestamps);
eval_okay_test!( eval_fail_test!(fail_fromTOML_valueWithNullByte);
#[ignore = "not implemented: flakeRefToString"] eval_fail_test!(fail_hashfile_missing);
flake_ref_to_string eval_fail_test!(fail_infinite_recursion_lambda);
); eval_fail_test!(fail_list);
eval_okay_test!(flatten); eval_fail_test!(fail_missing_arg);
eval_okay_test!(float); eval_fail_test!(fail_mutual_recursion);
eval_okay_test!(floor_ceil); eval_fail_test!(fail_nested_list_items);
eval_okay_test!(foldlStrict); eval_fail_test!(fail_nonexist_path);
eval_okay_test!(foldlStrict_lazy_elements); eval_fail_test!(fail_not_throws);
eval_okay_test!(foldlStrict_lazy_initial_accumulator); eval_fail_test!(fail_overflowing_add);
eval_okay_test!(fromjson); eval_fail_test!(fail_overflowing_div);
eval_okay_test!(fromjson_escapes); eval_fail_test!(fail_overflowing_mul);
eval_okay_test!(fromTOML); eval_fail_test!(fail_overflowing_sub);
eval_okay_test!( eval_fail_test!(fail_path_slash);
#[ignore = "timestamps are not supported"] eval_fail_test!(fail_pipe_operators);
fromTOML_timestamps eval_fail_test!(fail_recursion);
); eval_fail_test!(fail_remove);
eval_okay_test!(functionargs); eval_fail_test!(fail_scope_5);
eval_okay_test!(hashfile); eval_fail_test!(fail_seq);
eval_okay_test!(hashstring); eval_fail_test!(fail_set);
eval_okay_test!(getattrpos); eval_fail_test!(fail_set_override);
eval_okay_test!(getattrpos_functionargs); eval_fail_test!(fail_string_nul_1);
eval_okay_test!(getattrpos_undefined); eval_fail_test!(fail_string_nul_2);
eval_okay_test!( eval_fail_test!(fail_substring);
#[serial(env)] eval_fail_test!(fail_toJSON);
getenv, eval_fail_test!(fail_toJSON_non_utf_8);
|| { eval_fail_test!(fail_to_path);
// SAFETY: guarded with #[serial_test::serial] eval_fail_test!(fail_undeclared_arg);
unsafe { std::env::set_var("TEST_VAR", "foo") }; eval_fail_test!(fail_using_set_as_attr_name);
}
);
eval_okay_test!(groupBy);
eval_okay_test!(r#if);
eval_okay_test!(ind_string);
eval_okay_test!(import);
eval_okay_test!(inherit_attr_pos);
eval_okay_test!(
#[ignore = "__overrides is not supported"]
inherit_from
);
eval_okay_test!(intersectAttrs);
eval_okay_test!(r#let);
eval_okay_test!(list);
eval_okay_test!(listtoattrs);
eval_okay_test!(logic);
eval_okay_test!(map);
eval_okay_test!(mapattrs);
eval_okay_test!(merge_dynamic_attrs);
eval_okay_test!(nested_with);
eval_okay_test!(new_let);
eval_okay_test!(null_dynamic_attrs);
eval_okay_test!(
#[ignore = "__overrides is not supported"]
overrides
);
eval_okay_test!(
#[ignore = "not implemented: parseFlakeRef"]
parse_flake_ref
);
eval_okay_test!(partition);
eval_okay_test!(path);
eval_okay_test!(pathexists);
eval_okay_test!(
#[serial(env)]
path_string_interpolation,
|| {
// SAFETY: guarded with #[serial_test::serial]
unsafe {
std::env::set_var("HOME", "/fake-home");
}
}
);
eval_okay_test!(patterns);
eval_okay_test!(print);
eval_okay_test!(readDir);
eval_okay_test!(readfile);
eval_okay_test!(readFileType);
eval_okay_test!(redefine_builtin);
eval_okay_test!(regex_match);
eval_okay_test!(regex_split);
eval_okay_test!(regression_20220122);
eval_okay_test!(regression_20220125);
eval_okay_test!(regrettable_rec_attrset_merge);
eval_okay_test!(remove);
eval_okay_test!(repeated_empty_attrs);
eval_okay_test!(repeated_empty_list);
eval_okay_test!(replacestrings);
eval_okay_test!(
#[ignore = "requires -I CLI flags"]
search_path
);
eval_okay_test!(scope_1);
eval_okay_test!(scope_2);
eval_okay_test!(scope_3);
eval_okay_test!(scope_4);
eval_okay_test!(scope_6);
eval_okay_test!(scope_7);
eval_okay_test!(seq);
eval_okay_test!(sort);
eval_okay_test!(splitversion);
eval_okay_test!(string);
eval_okay_test!(strings_as_attrs_names);
eval_okay_test!(substring);
eval_okay_test!(substring_context);
eval_okay_test!(symlink_resolution);
eval_okay_test!(
#[ignore = "TCO not implemented, also disabled in CppNix"]
tail_call_1
);
eval_okay_test!(tojson);
eval_okay_test!(toxml);
eval_okay_test!(toxml2);
eval_okay_test!(tryeval);
eval_okay_test!(types);
eval_okay_test!(versions);
eval_okay_test!(with);
eval_okay_test!(zipAttrsWith);
}
mod fail {
use super::*;
eval_fail_test!(abort);
eval_fail_test!(addDrvOutputDependencies_empty_context);
eval_fail_test!(addDrvOutputDependencies_multi_elem_context);
eval_fail_test!(addDrvOutputDependencies_wrong_element_kind);
eval_fail_test!(addErrorRuntime_example);
eval_fail_test!(assert);
eval_fail_test!(assert_equal_attrs_names);
eval_fail_test!(assert_equal_attrs_names_2);
eval_fail_test!(assert_equal_derivations);
eval_fail_test!(assert_equal_derivations_extra);
eval_fail_test!(assert_equal_floats);
eval_fail_test!(assert_equal_function_direct);
eval_fail_test!(assert_equal_int_float);
eval_fail_test!(assert_equal_ints);
eval_fail_test!(assert_equal_list_length);
eval_fail_test!(assert_equal_paths);
eval_fail_test!(assert_equal_type);
eval_fail_test!(assert_equal_type_nested);
eval_fail_test!(assert_nested_bool);
eval_fail_test!(attr_name_type);
eval_fail_test!(attrset_merge_drops_later_rec);
eval_fail_test!(bad_string_interpolation_1);
eval_fail_test!(bad_string_interpolation_2);
eval_fail_test!(bad_string_interpolation_3);
eval_fail_test!(bad_string_interpolation_4);
eval_fail_test!(blackhole);
eval_fail_test!(call_primop);
eval_fail_test!(deepseq);
eval_fail_test!(derivation_name);
eval_fail_test!(dup_dynamic_attrs);
eval_fail_test!(duplicate_traces);
eval_fail_test!(eol_1);
eval_fail_test!(eol_2);
eval_fail_test!(eol_3);
eval_fail_test!(fetchTree_negative);
eval_fail_test!(fetchurl_baseName);
eval_fail_test!(fetchurl_baseName_attrs);
eval_fail_test!(fetchurl_baseName_attrs_name);
eval_fail_test!(flake_ref_to_string_negative_integer);
eval_fail_test!(foldlStrict_strict_op_application);
eval_fail_test!(fromJSON_keyWithNullByte);
eval_fail_test!(fromJSON_overflowing);
eval_fail_test!(fromJSON_valueWithNullByte);
eval_fail_test!(fromTOML_keyWithNullByte);
eval_fail_test!(fromTOML_timestamps);
eval_fail_test!(fromTOML_valueWithNullByte);
eval_fail_test!(hashfile_missing);
eval_fail_test!(infinite_recursion_lambda);
eval_fail_test!(list);
eval_fail_test!(missing_arg);
eval_fail_test!(mutual_recursion);
eval_fail_test!(nested_list_items);
eval_fail_test!(nonexist_path);
eval_fail_test!(not_throws);
eval_fail_test!(overflowing_add);
eval_fail_test!(overflowing_div);
eval_fail_test!(overflowing_mul);
eval_fail_test!(overflowing_sub);
eval_fail_test!(path_slash);
eval_fail_test!(pipe_operators);
eval_fail_test!(recursion);
eval_fail_test!(remove);
eval_fail_test!(scope_5);
eval_fail_test!(seq);
eval_fail_test!(set);
eval_fail_test!(set_override);
eval_fail_test!(string_nul_1);
eval_fail_test!(string_nul_2);
eval_fail_test!(substring);
eval_fail_test!(toJSON);
eval_fail_test!(toJSON_non_utf_8);
eval_fail_test!(to_path);
eval_fail_test!(undeclared_arg);
eval_fail_test!(using_set_as_attr_name);
}
-8
View File
@@ -1,11 +1,3 @@
#![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;
+13 -13
View File
@@ -1,5 +1,5 @@
use fix::Evaluator; use fix::runtime::Runtime;
use fix_lang::Value; use fix::value::Value;
use crate::utils::eval_result; use crate::utils::eval_result;
@@ -153,7 +153,7 @@ fn string_add_merges_context() {
#[test_log::test] #[test_log::test]
fn context_in_derivation_args() { fn context_in_derivation_args() {
let mut rt = Evaluator::new(); let mut rt = Runtime::new().unwrap();
let result = rt let result = rt
.eval( .eval(
r#" r#"
@@ -182,7 +182,7 @@ fn context_in_derivation_args() {
#[test_log::test] #[test_log::test]
fn context_in_derivation_env() { fn context_in_derivation_env() {
let mut rt = Evaluator::new(); let mut rt = Runtime::new().unwrap();
let result = rt let result = rt
.eval( .eval(
r#" r#"
@@ -224,7 +224,7 @@ fn tostring_preserves_context() {
#[test_log::test] #[test_log::test]
fn interpolation_derivation_returns_outpath() { fn interpolation_derivation_returns_outpath() {
let mut rt = Evaluator::new(); let mut rt = Runtime::new().unwrap();
let result = rt let result = rt
.eval( .eval(
r#" r#"
@@ -348,7 +348,7 @@ fn substring_zero_length_empty_value() {
} }
#[test_log::test] #[test_log::test]
#[expect(non_snake_case)] #[allow(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]
#[expect(non_snake_case)] #[allow(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]
#[expect(non_snake_case)] #[allow(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]
#[expect(non_snake_case)] #[allow(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]
#[expect(non_snake_case)] #[allow(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]
#[expect(non_snake_case)] #[allow(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]
#[expect(non_snake_case)] #[allow(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]
#[expect(non_snake_case)] #[allow(non_snake_case)]
fn baseNameOf_preserves_context() { fn baseNameOf_preserves_context() {
let result = eval( let result = eval(
r#" r#"
+17 -8
View File
@@ -1,29 +1,38 @@
use fix::Evaluator; #![allow(dead_code)]
use fix_error::{Result, Source};
use fix_lang::Value; use fix::error::{Result, Source};
use fix::runtime::Runtime;
use fix::value::Value;
pub fn eval(expr: &str) -> Value { pub fn eval(expr: &str) -> Value {
Evaluator::new() Runtime::new()
.unwrap()
.eval(Source::new_eval(expr.into()).unwrap()) .eval(Source::new_eval(expr.into()).unwrap())
.unwrap() .unwrap()
} }
pub fn eval_shallow(expr: &str) -> Value { pub fn eval_shallow(expr: &str) -> Value {
Evaluator::new() Runtime::new()
.unwrap()
.eval_shallow(Source::new_eval(expr.into()).unwrap()) .eval_shallow(Source::new_eval(expr.into()).unwrap())
.unwrap() .unwrap()
} }
pub fn eval_deep(expr: &str) -> Value { pub fn eval_deep(expr: &str) -> Value {
Evaluator::new() Runtime::new()
.unwrap()
.eval_deep(Source::new_eval(expr.into()).unwrap()) .eval_deep(Source::new_eval(expr.into()).unwrap())
.unwrap() .unwrap()
} }
pub fn eval_deep_result(expr: &str) -> Result<Value> { pub fn eval_deep_result(expr: &str) -> Result<Value> {
Evaluator::new().eval_deep(Source::new_eval(expr.into()).unwrap()) Runtime::new()
.unwrap()
.eval_deep(Source::new_eval(expr.into()).unwrap())
} }
pub fn eval_result(expr: &str) -> Result<Value> { pub fn eval_result(expr: &str) -> Result<Value> {
Evaluator::new().eval(Source::new_eval(expr.into()).unwrap()) Runtime::new()
.unwrap()
.eval(Source::new_eval(expr.into()).unwrap())
} }
Generated
+13 -13
View File
@@ -8,11 +8,11 @@
"rust-analyzer-src": "rust-analyzer-src" "rust-analyzer-src": "rust-analyzer-src"
}, },
"locked": { "locked": {
"lastModified": 1784017020, "lastModified": 1774596377,
"narHash": "sha256-49WO85egjNtN1vMgJ3zUjDh5IqO+ou4zZY80WQ6EKGg=", "narHash": "sha256-DiSLMxyTwIUAlhOe34r6kKNQRv6PTF+vf0MG45mAyn4=",
"owner": "nix-community", "owner": "nix-community",
"repo": "fenix", "repo": "fenix",
"rev": "fa2a0be0f712d7147d1677d33d15ea7b0589cdb4", "rev": "a88a1c8cf2f094da6347fcec54089f4bcb518409",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -24,11 +24,11 @@
"flake-compat": { "flake-compat": {
"flake": false, "flake": false,
"locked": { "locked": {
"lastModified": 1777699697, "lastModified": 1751685974,
"narHash": "sha256-Eg9b/rq/ECYwNwEXs5i9wHyhxNI0JrYx2srdI2uZMaQ=", "narHash": "sha256-NKw96t+BgHIYzHUjkTK95FqYRVKB8DHpVhefWSz/kTw=",
"rev": "382052b74656a369c5408822af3f2501e9b1af81", "rev": "549f2762aebeff29a2e5ece7a7dc0f955281a1d1",
"type": "tarball", "type": "tarball",
"url": "https://git.lix.systems/api/v1/repos/lix-project/flake-compat/archive/382052b74656a369c5408822af3f2501e9b1af81.tar.gz" "url": "https://git.lix.systems/api/v1/repos/lix-project/flake-compat/archive/549f2762aebeff29a2e5ece7a7dc0f955281a1d1.tar.gz"
}, },
"original": { "original": {
"type": "tarball", "type": "tarball",
@@ -37,11 +37,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1784007870, "lastModified": 1774386573,
"narHash": "sha256-djcLt/JJphyNt4eDY9XTly+/WbCK5lqWq9lSgCmJkkQ=", "narHash": "sha256-4hAV26quOxdC6iyG7kYaZcM3VOskcPUrdCQd/nx8obc=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "18b9261cb3294b6d2a06d03f96872827b8fe2698", "rev": "46db2e09e1d3f113a13c0d7b81e2f221c63b8ce9",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -61,11 +61,11 @@
"rust-analyzer-src": { "rust-analyzer-src": {
"flake": false, "flake": false,
"locked": { "locked": {
"lastModified": 1783975201, "lastModified": 1774569884,
"narHash": "sha256-oyHWTfKWk06nTynCO8ByEAx/KEARvOOyTVnwinOmK3Q=", "narHash": "sha256-E8iWEPzg7OnE0XXXjo75CX7xFauqzJuGZ5wSO9KS8Ek=",
"owner": "rust-lang", "owner": "rust-lang",
"repo": "rust-analyzer", "repo": "rust-analyzer",
"rev": "63a6f0d4bcfd3bbcf36383fcbcbcd93456ed1653", "rev": "443ddcddd0c73b07b799d052f5ef3b448c2f3508",
"type": "github" "type": "github"
}, },
"original": { "original": {
+32 -12
View File
@@ -8,23 +8,43 @@
flake = false; flake = false;
}; };
}; };
outputs = outputs = { nixpkgs, fenix, ... }:
{ nixpkgs, fenix, ... }:
let let
forAllSystems = nixpkgs.lib.genAttrs nixpkgs.lib.systems.flakeExposed; forAllSystems = nixpkgs.lib.genAttrs nixpkgs.lib.systems.flakeExposed;
in in
{ {
devShells = forAllSystems ( devShells = forAllSystems (system:
system: let pkgs = import nixpkgs { inherit system; config.allowUnfree = true; }; in
let
pkgs = import nixpkgs {
inherit system;
config.allowUnfree = true;
overlays = [ fenix.overlays.default ];
};
in
{ {
default = import ./devShell.nix { inherit pkgs; }; default = pkgs.mkShell {
packages = with pkgs; [
(fenix.packages.${system}.latest.withComponents [
"cargo"
"clippy"
"rust-src"
"rustc"
"rustfmt"
"rust-analyzer"
])
cargo-outdated
cargo-machete
lldb
valgrind
hyperfine
just
samply
jq
tokei
nodejs
nodePackages.npm
biome
claude-code
codex
opencode
];
};
} }
); );
}; };
+16
View File
@@ -0,0 +1,16 @@
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
+2 -2
View File
@@ -1,7 +1,7 @@
[files] [files]
extend-exclude = [ extend-exclude = [
"nix-js/tests/tests/regex.rs", "nix-js/tests/tests/regex.rs",
"nix-js/tests/tests/lang", "nix-js/tests/tests/lang",
] ]
[default.extend-words] [default.extend-words]