Compare commits
1
Commits
new
..
e4c65d43ad
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e4c65d43ad
|
@@ -10,4 +10,3 @@ prof.json
|
||||
*.cpuprofile
|
||||
*.cpuprofile.gz
|
||||
*v8.log*
|
||||
callgrind.*
|
||||
|
||||
@@ -16,9 +16,6 @@ vim.lsp.config("rust_analyzer", {
|
||||
settings = {
|
||||
["rust-analyzer"] = {
|
||||
cargo = {
|
||||
features = {
|
||||
"tailcall"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+2384
-514
File diff suppressed because it is too large
Load Diff
+1
-95
@@ -1,103 +1,9 @@
|
||||
[workspace]
|
||||
resolver = "3"
|
||||
members = [
|
||||
"fix",
|
||||
"fix-bytecode",
|
||||
"fix-compiler",
|
||||
"fix-error",
|
||||
"fix-lang",
|
||||
"fix-macros",
|
||||
"fix-runtime",
|
||||
"fix-vm",
|
||||
"fix",
|
||||
]
|
||||
|
||||
[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]
|
||||
inherits = "release"
|
||||
debug = true
|
||||
|
||||
@@ -15,6 +15,17 @@
|
||||
cargo run --release -- eval --expr '{{expr}}'
|
||||
|
||||
[no-exit-message]
|
||||
[positional-arguments]
|
||||
@cg *args='':
|
||||
valgrind --tool=callgrind --dump-instr=yes --collect-jumps=yes "$@"
|
||||
@repli:
|
||||
cargo run --release --features inspector -- --inspect-brk 127.0.0.1:9229 repl
|
||||
|
||||
[no-exit-message]
|
||||
@evali expr:
|
||||
cargo run --release --features inspector -- --inspect-brk 127.0.0.1:9229 eval --expr '{{expr}}'
|
||||
|
||||
[no-exit-message]
|
||||
@replp:
|
||||
cargo run --release --features prof -- repl
|
||||
|
||||
[no-exit-message]
|
||||
@evalp expr:
|
||||
cargo run --release --features prof -- eval --expr '{{expr}}'
|
||||
|
||||
+70
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
];
|
||||
}
|
||||
@@ -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
|
||||
@@ -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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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>>,
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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::*;
|
||||
@@ -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 {}
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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())
|
||||
))),
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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::*;
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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"),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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<'_>>()
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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:?}"),
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
@@ -3,24 +3,23 @@ name = "fix"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[[bench]]
|
||||
harness = false
|
||||
name = "basic_ops"
|
||||
|
||||
[[bench]]
|
||||
harness = false
|
||||
name = "builtins"
|
||||
|
||||
[[bench]]
|
||||
harness = false
|
||||
name = "thunk_scope"
|
||||
|
||||
[dependencies]
|
||||
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
|
||||
anyhow = "1.0"
|
||||
rustyline = "18.0"
|
||||
rustyline = "17.0"
|
||||
|
||||
# CLI
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
@@ -29,28 +28,77 @@ clap = { version = "4", features = ["derive"] }
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
|
||||
miette = { version = "7.4", features = ["fancy"] }
|
||||
# Error Reporting
|
||||
derive_more = { version = "2", features = ["full"] }
|
||||
thiserror = "2"
|
||||
miette = { version = "7.4", features = ["fancy"] }
|
||||
|
||||
# Data Structure
|
||||
hashbrown = { workspace = true }
|
||||
string-interner = { workspace = true }
|
||||
hashbrown = "0.16"
|
||||
string-interner = "0.19"
|
||||
bumpalo = { version = "3.20", features = [
|
||||
"allocator-api2",
|
||||
"boxed",
|
||||
"collections",
|
||||
] }
|
||||
|
||||
ere = { workspace = true }
|
||||
rust-embed = "8.11"
|
||||
|
||||
fix-bytecode = { path = "../fix-bytecode" }
|
||||
fix-compiler = { path = "../fix-compiler" }
|
||||
fix-error = { path = "../fix-error" }
|
||||
fix-lang = { path = "../fix-lang" }
|
||||
fix-runtime = { path = "../fix-runtime" }
|
||||
fix-vm = { path = "../fix-vm" }
|
||||
regex = "1.11"
|
||||
|
||||
nix-nar = "0.3"
|
||||
sha2 = "0.10"
|
||||
sha1 = "0.10"
|
||||
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]
|
||||
criterion = { version = "0.8", features = ["html_reports"] }
|
||||
serial_test = "4.0"
|
||||
tempfile = "3.24"
|
||||
test-log = { version = "0.2", features = ["trace"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
[[bench]]
|
||||
name = "basic_ops"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "builtins"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "thunk_scope"
|
||||
harness = false
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
#![allow(clippy::allow_attributes_without_reason)]
|
||||
#![allow(dead_code, clippy::unwrap_used, clippy::unwrap_in_result)]
|
||||
#![allow(dead_code)]
|
||||
|
||||
use fix::Evaluator;
|
||||
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 {
|
||||
Evaluator::new()
|
||||
Runtime::new()
|
||||
.unwrap()
|
||||
.eval(Source::new_eval(expr.into()).unwrap())
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
|
||||
@@ -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::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 from_val(value: &Value) -> Self;
|
||||
}
|
||||
@@ -82,48 +77,41 @@ int_store!(i16);
|
||||
int_store!(i32);
|
||||
|
||||
fn store_ptr<P: Strict + Copy>(value: &mut Value, ptr: P) {
|
||||
cfg_select! {
|
||||
target_pointer_width = "64" => {
|
||||
assert!(
|
||||
ptr.addr() <= 0x0000_FFFF_FFFF_FFFF,
|
||||
"Pointer too large to store in NaN box"
|
||||
);
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
{
|
||||
assert!(
|
||||
ptr.addr() <= 0x0000_FFFF_FFFF_FFFF,
|
||||
"Pointer too large to store in NaN box"
|
||||
);
|
||||
|
||||
// SAFETY: `Value` is `#[repr(C, align(8))]` and exactly 8 bytes, so it is
|
||||
// sound to reinterpret its storage as a `[u8; 8]` that we then treat as `P`
|
||||
// (a pointer-sized value).
|
||||
let val = (unsafe { value.whole_mut() } as *mut [u8; 8]).cast::<P>();
|
||||
let val = (unsafe { value.whole_mut() } as *mut [u8; 8]).cast::<P>();
|
||||
|
||||
let ptr = Strict::map_addr(ptr, |addr| {
|
||||
addr | (usize::from(value.header().into_raw()) << 48)
|
||||
});
|
||||
let ptr = Strict::map_addr(ptr, |addr| {
|
||||
addr | (usize::from(value.header().into_raw()) << 48)
|
||||
});
|
||||
|
||||
// SAFETY: `val` points to the `Value`'s 8-byte storage, which is valid and
|
||||
// suitably aligned to hold `P`.
|
||||
unsafe { val.write(ptr) };
|
||||
}
|
||||
_ => {
|
||||
compile_error!("unsupported pointer width");
|
||||
}
|
||||
unsafe { val.write(ptr) };
|
||||
}
|
||||
|
||||
#[cfg(target_pointer_width = "32")]
|
||||
{
|
||||
let _ = (value, ptr);
|
||||
unimplemented!("32-bit pointer storage not supported");
|
||||
}
|
||||
}
|
||||
|
||||
fn load_ptr<P: Strict>(value: &Value) -> P {
|
||||
cfg_select! {
|
||||
target_pointer_width = "64" => {
|
||||
// SAFETY: `Value` is `#[repr(C, align(8))]` and exactly 8 bytes, so it
|
||||
// is sound to reinterpret its storage as a `[u8; 8]` cast to `P`; the
|
||||
// pointer was originally written through this same `P` layout by
|
||||
// `store_ptr`.
|
||||
let val = (unsafe { value.whole() } as *const [u8; 8]).cast::<P>();
|
||||
// SAFETY: `val` points to the `Value`'s 8-byte storage, which is valid
|
||||
// and suitably aligned to hold `P`.
|
||||
let ptr = unsafe { val.read() };
|
||||
Strict::map_addr(ptr, |addr| addr & 0x0000_FFFF_FFFF_FFFF)
|
||||
}
|
||||
_ => {
|
||||
compile_error!("unsupported pointer width");
|
||||
}
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
{
|
||||
let val = (unsafe { value.whole() } as *const [u8; 8]).cast::<P>();
|
||||
let ptr = unsafe { val.read() };
|
||||
Strict::map_addr(ptr, |addr| addr & 0x0000_FFFF_FFFF_FFFF)
|
||||
}
|
||||
|
||||
#[cfg(target_pointer_width = "32")]
|
||||
{
|
||||
let _ = value;
|
||||
unimplemented!("32-bit pointer storage not supported");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,32 +155,12 @@ enum TagVal {
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
pub struct RawTag(TagVal);
|
||||
pub(crate) struct RawTag(TagVal);
|
||||
|
||||
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]
|
||||
#[must_use]
|
||||
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) }
|
||||
}
|
||||
|
||||
@@ -225,7 +193,7 @@ impl RawTag {
|
||||
/// `val` must be in the range `1..8`
|
||||
#[inline]
|
||||
#[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) {
|
||||
(false, 1) => TagVal::_P1,
|
||||
(false, 2) => TagVal::_P2,
|
||||
@@ -243,9 +211,6 @@ impl RawTag {
|
||||
(true, 6) => TagVal::_N6,
|
||||
(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() },
|
||||
})
|
||||
}
|
||||
@@ -277,7 +242,7 @@ impl RawTag {
|
||||
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub const fn neg_val(self) -> (bool, u8) {
|
||||
pub(crate) fn neg_val(self) -> (bool, u8) {
|
||||
match self.0 {
|
||||
TagVal::_P1 => (false, 1),
|
||||
TagVal::_P2 => (false, 2),
|
||||
@@ -318,21 +283,17 @@ impl Header {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
const fn tag(self) -> RawTag {
|
||||
// SAFETY: a `Header` is only ever constructed by `Header::new` from a
|
||||
// `RawTag` whose value is in `1..8`, stored in the low three bits;
|
||||
// `get_tag` recovers exactly those bits, so the argument passed to
|
||||
// `new_unchecked` is in the required `1..8` range.
|
||||
fn tag(self) -> RawTag {
|
||||
unsafe { RawTag::new_unchecked(self.get_sign(), self.get_tag()) }
|
||||
}
|
||||
|
||||
#[inline]
|
||||
const fn get_sign(self) -> bool {
|
||||
fn get_sign(self) -> bool {
|
||||
self.0 & 0x8000 != 0
|
||||
}
|
||||
|
||||
#[inline]
|
||||
const fn get_tag(self) -> u8 {
|
||||
fn get_tag(self) -> u8 {
|
||||
(self.0 & 0x0007) as u8
|
||||
}
|
||||
|
||||
@@ -378,7 +339,7 @@ impl Value {
|
||||
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub(crate) const fn tag(&self) -> RawTag {
|
||||
pub(crate) fn tag(&self) -> RawTag {
|
||||
self.header.tag()
|
||||
}
|
||||
|
||||
@@ -394,7 +355,7 @@ impl Value {
|
||||
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn data(&self) -> &[u8; 6] {
|
||||
pub(crate) fn data(&self) -> &[u8; 6] {
|
||||
&self.data
|
||||
}
|
||||
|
||||
@@ -408,9 +369,6 @@ impl Value {
|
||||
#[must_use]
|
||||
unsafe fn whole(&self) -> &[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 }
|
||||
}
|
||||
|
||||
@@ -418,10 +376,6 @@ impl Value {
|
||||
#[must_use]
|
||||
unsafe fn whole_mut(&mut self) -> &mut [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 }
|
||||
}
|
||||
}
|
||||
@@ -461,11 +415,8 @@ impl RawBox {
|
||||
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub(crate) const fn tag(&self) -> Option<RawTag> {
|
||||
pub(crate) fn tag(&self) -> Option<RawTag> {
|
||||
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() })
|
||||
} else {
|
||||
None
|
||||
@@ -475,18 +426,12 @@ impl RawBox {
|
||||
#[inline]
|
||||
#[must_use]
|
||||
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 })
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub(crate) const fn is_value(&self) -> bool {
|
||||
// SAFETY: every 8-byte pattern is simultaneously a valid `f64` and a
|
||||
// valid `u64`, so reading the `float` and `bits` union fields is
|
||||
// always sound.
|
||||
pub(crate) fn is_value(&self) -> bool {
|
||||
(unsafe { self.float.is_nan() } && unsafe { self.bits & SIGN_MASK != QUIET_NAN })
|
||||
}
|
||||
|
||||
@@ -494,8 +439,6 @@ impl RawBox {
|
||||
#[must_use]
|
||||
pub(crate) fn float(&self) -> Option<&f64> {
|
||||
if self.is_float() {
|
||||
// SAFETY: reading the `float` field is sound because any 8-byte
|
||||
// pattern is a valid `f64`.
|
||||
Some(unsafe { &self.float })
|
||||
} else {
|
||||
None
|
||||
@@ -506,9 +449,6 @@ impl RawBox {
|
||||
#[must_use]
|
||||
pub(crate) fn value(&self) -> Option<&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 })
|
||||
} else {
|
||||
None
|
||||
@@ -517,16 +457,12 @@ impl RawBox {
|
||||
|
||||
#[inline]
|
||||
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 }
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[must_use]
|
||||
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 }
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,108 @@
|
||||
use fix_bytecode::{Const, InstructionPtr, Op, OperandType};
|
||||
use fix_lang::StringId;
|
||||
use std::ops::Deref;
|
||||
|
||||
use gc_arena::Collect;
|
||||
use hashbrown::HashMap;
|
||||
use num_enum::TryFromPrimitive;
|
||||
use rnix::TextRange;
|
||||
use string_interner::Symbol as _;
|
||||
|
||||
mod context;
|
||||
pub mod ir;
|
||||
pub use context::{CodeState, ExtraScope};
|
||||
pub use fix_bytecode::disassembler;
|
||||
pub use ir::{Attr, BinOpKind, Ir, MaybeThunk, Param, RawIrRef, ThunkId, UnOpKind};
|
||||
use crate::ir::{ArgId, Attr, BinOpKind, Ir, Param, RawIrRef, StringId, ThunkId, UnOpKind};
|
||||
use crate::runtime::BUILTINS;
|
||||
use crate::runtime::value::{Null, PrimOp, StaticValue};
|
||||
|
||||
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 register_span(&mut self, range: TextRange) -> u32;
|
||||
fn get_code(&self) -> &[u8];
|
||||
fn get_code_mut(&mut self) -> &mut Vec<u8>;
|
||||
fn add_constant(&mut self, val: Const) -> u32;
|
||||
fn current_source_dir(&mut self) -> StringId;
|
||||
fn add_constant(&mut self, val: crate::runtime::value::StaticValue) -> u32;
|
||||
}
|
||||
|
||||
#[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 {
|
||||
depth: u8,
|
||||
depth: u16,
|
||||
arg_id: Option<ArgId>,
|
||||
thunk_map: HashMap<ThunkId, u32>,
|
||||
}
|
||||
|
||||
@@ -29,17 +111,22 @@ struct BytecodeEmitter<'a, Ctx: BytecodeContext> {
|
||||
scope_stack: Vec<ScopeInfo>,
|
||||
}
|
||||
|
||||
pub enum InlineOperand {
|
||||
Const(Const),
|
||||
BigInt(i64),
|
||||
Local { layer: u8, local: u32 },
|
||||
BuiltinConst(StringId),
|
||||
pub(crate) const OPERAND_CONST: u8 = 0;
|
||||
pub(crate) const OPERAND_LOCAL: u8 = 1;
|
||||
pub(crate) const OPERAND_BUILTINS: u8 = 2;
|
||||
pub(crate) const OPERAND_BIGINT: u8 = 3;
|
||||
|
||||
pub(crate) const KEY_STATIC: u8 = 0;
|
||||
pub(crate) const KEY_DYNAMIC: u8 = 1;
|
||||
|
||||
enum InlineOperand {
|
||||
Const(StaticValue),
|
||||
Local { layer: u16, local: u32 },
|
||||
Builtins,
|
||||
ReplBinding(StringId),
|
||||
ScopedImportBinding { id: StringId, slot_id: u32 },
|
||||
BigInt(i64),
|
||||
}
|
||||
|
||||
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 mut emitter = BytecodeEmitter::new(ctx);
|
||||
emitter.emit_toplevel(ir);
|
||||
@@ -54,69 +141,57 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
|
||||
}
|
||||
}
|
||||
|
||||
fn inline_maybe_thunk(&self, val: &MaybeThunk) -> InlineOperand {
|
||||
use MaybeThunk::*;
|
||||
match *val {
|
||||
Int(x) => {
|
||||
if let Ok(x) = x.try_into() {
|
||||
InlineOperand::Const(Const::Smi(x))
|
||||
fn classify_value(&mut self, ir: RawIrRef<'_>) -> InlineOperand {
|
||||
match ir.deref() {
|
||||
&Ir::Int(x) => {
|
||||
if x <= i32::MAX as i64 {
|
||||
InlineOperand::Const(StaticValue::new_inline(x as i32))
|
||||
} else {
|
||||
InlineOperand::BigInt(x)
|
||||
}
|
||||
}
|
||||
Float(x) => InlineOperand::Const(Const::Float(x)),
|
||||
Bool(b) => InlineOperand::Const(Const::Bool(b)),
|
||||
Null => InlineOperand::Const(Const::Null),
|
||||
Str(id) => InlineOperand::Const(Const::String(id)),
|
||||
Path(id) => InlineOperand::Const(Const::String(id)),
|
||||
Thunk(id) => {
|
||||
&Ir::Float(x) => InlineOperand::Const(StaticValue::new_float(x)),
|
||||
&Ir::Bool(b) => InlineOperand::Const(StaticValue::new_inline(b)),
|
||||
Ir::Null => InlineOperand::Const(StaticValue::new_inline(Null)),
|
||||
Ir::Str(s) => {
|
||||
let sid = self.ctx.intern_string(s.deref());
|
||||
InlineOperand::Const(StaticValue::new_inline(sid))
|
||||
}
|
||||
&Ir::Thunk(id) => {
|
||||
let (layer, local) = self.resolve_thunk(id);
|
||||
InlineOperand::Local { layer, local }
|
||||
}
|
||||
Arg { layer } => InlineOperand::Local { layer, local: 0 },
|
||||
Builtin(id) => InlineOperand::Const(Const::PrimOp(id)),
|
||||
BuiltinConst(id) => InlineOperand::BuiltinConst(id),
|
||||
Builtins => InlineOperand::Builtins,
|
||||
ReplBinding(id) => InlineOperand::ReplBinding(id),
|
||||
ScopedImportBinding { slot_id, sym: id } => {
|
||||
InlineOperand::ScopedImportBinding { slot_id, id }
|
||||
&Ir::Arg(id) => {
|
||||
let (layer, local) = self.resolve_arg(id);
|
||||
InlineOperand::Local { layer, local }
|
||||
}
|
||||
&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) {
|
||||
use InlineOperand::*;
|
||||
let operand = self.inline_maybe_thunk(val);
|
||||
fn emit_inline_operand(&mut self, operand: InlineOperand) {
|
||||
match operand {
|
||||
Const(val) => {
|
||||
InlineOperand::Const(val) => {
|
||||
let idx = self.ctx.add_constant(val);
|
||||
self.emit_u8(OperandType::Const as u8);
|
||||
self.emit_u8(OPERAND_CONST);
|
||||
self.emit_u32(idx);
|
||||
}
|
||||
BigInt(val) => {
|
||||
self.emit_u8(OperandType::BigInt as u8);
|
||||
self.emit_i64(val);
|
||||
}
|
||||
Local { layer, local } => {
|
||||
self.emit_u8(OperandType::Local as u8);
|
||||
self.emit_u8(layer);
|
||||
InlineOperand::Local { layer, local } => {
|
||||
self.emit_u8(OPERAND_LOCAL);
|
||||
self.emit_u8(layer as u8);
|
||||
self.emit_u32(local);
|
||||
}
|
||||
BuiltinConst(id) => {
|
||||
self.emit_u8(OperandType::BuiltinConst as u8);
|
||||
self.emit_str_id(id);
|
||||
InlineOperand::Builtins => {
|
||||
self.emit_u8(OPERAND_BUILTINS);
|
||||
}
|
||||
Builtins => {
|
||||
self.emit_u8(OperandType::Builtins as u8);
|
||||
}
|
||||
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);
|
||||
InlineOperand::BigInt(val) => {
|
||||
self.emit_u8(OPERAND_BIGINT);
|
||||
self.emit_i64(val);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -126,11 +201,6 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
|
||||
self.ctx.get_code_mut().push(op as u8);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn emit_bool(&mut self, val: bool) {
|
||||
self.emit_u8(u8::from(val));
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn emit_u8(&mut self, val: u8) {
|
||||
self.ctx.get_code_mut().push(val);
|
||||
@@ -178,10 +248,6 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
|
||||
offset
|
||||
}
|
||||
#[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) {
|
||||
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());
|
||||
}
|
||||
|
||||
fn current_depth(&self) -> u8 {
|
||||
fn current_depth(&self) -> u16 {
|
||||
self.scope_stack.last().map_or(0, |s| s.depth)
|
||||
}
|
||||
|
||||
#[expect(
|
||||
clippy::panic,
|
||||
reason = "a ThunkId must resolve in some enclosing scope; failure indicates a compiler bug"
|
||||
)]
|
||||
fn resolve_thunk(&self, id: ThunkId) -> (u8, u32) {
|
||||
fn resolve_thunk(&self, id: ThunkId) -> (u16, u32) {
|
||||
for scope in self.scope_stack.iter().rev() {
|
||||
if let Some(&local_idx) = scope.thunk_map.get(&id) {
|
||||
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);
|
||||
}
|
||||
|
||||
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 {
|
||||
self.emit_op(Op::LoadLocal);
|
||||
self.emit_u32(local);
|
||||
} else {
|
||||
self.emit_op(Op::LoadOuter);
|
||||
self.emit_u8(layer);
|
||||
self.emit_u8(layer as u8);
|
||||
self.emit_u32(local);
|
||||
}
|
||||
}
|
||||
|
||||
fn push_scope(&mut self, has_arg: bool, thunk_ids: &[ThunkId]) {
|
||||
let depth = self.scope_stack.len().try_into().expect("scope too deep!");
|
||||
fn count_with_thunks(&self, ir: RawIrRef<'_>) -> usize {
|
||||
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_map = thunk_ids
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, &id)| (id, thunk_base + i as u32))
|
||||
.collect();
|
||||
self.scope_stack.push(ScopeInfo { depth, thunk_map });
|
||||
self.scope_stack.push(ScopeInfo {
|
||||
depth,
|
||||
arg_id,
|
||||
thunk_map,
|
||||
});
|
||||
}
|
||||
|
||||
fn pop_scope(&mut self) {
|
||||
@@ -251,13 +453,19 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
|
||||
}
|
||||
|
||||
fn emit_toplevel(&mut self, ir: RawIrRef<'_>) {
|
||||
match ir {
|
||||
match ir.deref() {
|
||||
&Ir::TopLevel { body, ref thunks } => {
|
||||
let thunk_ids: Vec<ThunkId> = thunks.iter().map(|&(id, _)| id).collect();
|
||||
self.push_scope(false, &thunk_ids);
|
||||
if !thunks.is_empty() {
|
||||
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();
|
||||
|
||||
self.push_scope(false, None, &thunk_ids);
|
||||
|
||||
if total_slots > 0 {
|
||||
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);
|
||||
@@ -267,7 +475,7 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
|
||||
self.pop_scope();
|
||||
}
|
||||
_ => {
|
||||
self.push_scope(false, &[]);
|
||||
self.push_scope(false, None, &[]);
|
||||
self.emit_expr(ir);
|
||||
self.emit_op(Op::Return);
|
||||
self.pop_scope();
|
||||
@@ -277,6 +485,9 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
|
||||
|
||||
fn emit_scope_thunks(&mut self, thunks: &[(ThunkId, RawIrRef<'_>)]) {
|
||||
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 entry_point = self.ctx.get_code_mut().len() as u32;
|
||||
self.emit_expr(inner);
|
||||
@@ -284,6 +495,7 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
|
||||
self.patch_jump_target(skip_patch);
|
||||
self.emit_op(Op::MakeThunk);
|
||||
self.emit_u32(entry_point);
|
||||
self.emit_str_id(label_idx);
|
||||
let (_, local_idx) = self.resolve_thunk(id);
|
||||
self.emit_op(Op::StoreLocal);
|
||||
self.emit_u32(local_idx);
|
||||
@@ -291,11 +503,11 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
|
||||
}
|
||||
|
||||
fn emit_expr(&mut self, ir: RawIrRef<'_>) {
|
||||
match ir {
|
||||
match ir.deref() {
|
||||
&Ir::Int(x) => {
|
||||
if let Ok(x) = x.try_into() {
|
||||
if x <= i32::MAX as i64 {
|
||||
self.emit_op(Op::PushSmi);
|
||||
self.emit_i32(x);
|
||||
self.emit_i32(x as i32);
|
||||
} else {
|
||||
self.emit_op(Op::PushBigInt);
|
||||
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(false) => self.emit_op(Op::PushFalse),
|
||||
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_str_id(id);
|
||||
self.emit_str_id(idx);
|
||||
}
|
||||
&Ir::Path(p) => {
|
||||
self.emit_expr(p);
|
||||
self.emit_op(Op::ResolvePath);
|
||||
let dir_id = self.ctx.current_source_dir();
|
||||
self.emit_str_id(dir_id);
|
||||
}
|
||||
&Ir::If { cond, consq, alter } => {
|
||||
self.emit_expr(cond);
|
||||
@@ -355,9 +566,10 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
|
||||
&Ir::Func {
|
||||
body,
|
||||
ref param,
|
||||
arg,
|
||||
ref thunks,
|
||||
} => {
|
||||
self.emit_func(thunks, param, body);
|
||||
self.emit_func(arg, thunks, param, body);
|
||||
}
|
||||
Ir::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_u32(items.len() as u32);
|
||||
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);
|
||||
let span_id = self.ctx.register_span(span);
|
||||
self.emit_op(Op::Call);
|
||||
self.emit_maybe_thunk(arg);
|
||||
self.emit_u32(span_id);
|
||||
}
|
||||
&Ir::Arg { layer } => {
|
||||
self.emit_load(layer, 0);
|
||||
&Ir::Arg(id) => {
|
||||
let (layer, local) = self.resolve_arg(id);
|
||||
self.emit_load(layer, local);
|
||||
}
|
||||
&Ir::TopLevel { body, ref 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);
|
||||
}
|
||||
&Ir::Thunk(id) => {
|
||||
let (layer, local) = self.resolve_thunk(id);
|
||||
self.emit_load(layer, local);
|
||||
}
|
||||
Ir::Builtins => {
|
||||
self.emit_op(Op::LoadBuiltins);
|
||||
}
|
||||
@@ -401,7 +621,7 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
|
||||
}
|
||||
&Ir::BuiltinConst(id) => {
|
||||
self.emit_select(
|
||||
&Ir::Builtins,
|
||||
RawIrRef(&Ir::Builtins),
|
||||
&[Attr::Str(id, TextRange::default())],
|
||||
None,
|
||||
TextRange::default(),
|
||||
@@ -411,13 +631,13 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
|
||||
ref parts,
|
||||
force_string,
|
||||
} => {
|
||||
for &part in parts.iter() {
|
||||
self.emit_expr(part);
|
||||
self.emit_op(Op::CoerceToString);
|
||||
}
|
||||
self.emit_op(Op::ConcatStrings);
|
||||
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 } => {
|
||||
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 span_id = self.ctx.register_span(*span);
|
||||
self.emit_expr(*assertion);
|
||||
self.emit_expr(*expr);
|
||||
self.emit_op(Op::Assert);
|
||||
self.emit_str_id(raw_idx);
|
||||
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) => {
|
||||
self.emit_op(Op::LoadReplBinding);
|
||||
self.emit_str_id(name);
|
||||
}
|
||||
&Ir::ScopedImportBinding { sym, slot_id } => {
|
||||
&Ir::ScopedImportBinding(name) => {
|
||||
self.emit_op(Op::LoadScopedBinding);
|
||||
self.emit_u32(slot_id);
|
||||
self.emit_str_id(sym);
|
||||
self.emit_str_id(name);
|
||||
}
|
||||
Ir::WithLookup { sym, namespaces } => {
|
||||
// counter
|
||||
self.emit_expr(&Ir::Int(0));
|
||||
self.emit_op(Op::LookupWith);
|
||||
self.emit_str_id(*sym);
|
||||
self.emit_u8(
|
||||
namespaces
|
||||
.len()
|
||||
.try_into()
|
||||
.expect("too many `with` namespaces"),
|
||||
);
|
||||
for namespace in namespaces {
|
||||
self.emit_maybe_thunk(namespace);
|
||||
}
|
||||
&Ir::With {
|
||||
namespace,
|
||||
body,
|
||||
ref thunks,
|
||||
} => {
|
||||
self.emit_with(namespace, body, thunks);
|
||||
}
|
||||
&Ir::MaybeThunk(thunk) => {
|
||||
use MaybeThunk::*;
|
||||
match *thunk {
|
||||
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);
|
||||
}
|
||||
}
|
||||
&Ir::WithLookup(name) => {
|
||||
self.emit_op(Op::WithLookup);
|
||||
self.emit_str_id(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[expect(
|
||||
clippy::unreachable,
|
||||
reason = "the outer match only reaches this arm for the binary operator kinds enumerated above"
|
||||
)]
|
||||
fn emit_binop(&mut self, lhs: RawIrRef<'_>, rhs: RawIrRef<'_>, kind: BinOpKind) {
|
||||
use BinOpKind::*;
|
||||
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);
|
||||
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(rhs);
|
||||
@@ -606,17 +775,22 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
|
||||
}
|
||||
}
|
||||
|
||||
fn emit_func<'ir>(
|
||||
fn emit_func(
|
||||
&mut self,
|
||||
thunks: &[(ThunkId, RawIrRef<'ir>)],
|
||||
param: &Option<Param<'ir>>,
|
||||
body: RawIrRef<'ir>,
|
||||
arg: ArgId,
|
||||
thunks: &[(ThunkId, RawIrRef<'_>)],
|
||||
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 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_expr(body);
|
||||
self.emit_op(Op::Return);
|
||||
@@ -631,10 +805,10 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
|
||||
{
|
||||
self.emit_op(Op::MakePatternClosure);
|
||||
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(optional.len() as u16);
|
||||
self.emit_bool(*ellipsis);
|
||||
self.emit_u8(if *ellipsis { 1 } else { 0 });
|
||||
|
||||
for &(sym, _) in required.iter() {
|
||||
self.emit_str_id(sym);
|
||||
@@ -650,37 +824,38 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
|
||||
} else {
|
||||
self.emit_op(Op::MakeClosure);
|
||||
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(
|
||||
&mut self,
|
||||
stcs: &ir::HashMap<'_, StringId, (&MaybeThunk, TextRange)>,
|
||||
dyns: &[(RawIrRef<'_>, &MaybeThunk, TextRange)],
|
||||
stcs: &crate::ir::HashMap<'_, StringId, (RawIrRef<'_>, TextRange)>,
|
||||
dyns: &[(RawIrRef<'_>, RawIrRef<'_>, TextRange)],
|
||||
) {
|
||||
if stcs.is_empty() && dyns.is_empty() {
|
||||
self.emit_op(Op::MakeEmptyAttrs);
|
||||
return;
|
||||
}
|
||||
|
||||
for &(key_expr, _val, _span) in dyns.iter() {
|
||||
self.emit_expr(key_expr);
|
||||
}
|
||||
|
||||
let total = stcs.len() + dyns.len();
|
||||
self.emit_op(Op::MakeAttrs);
|
||||
self.emit_u32(stcs.len() as u32);
|
||||
self.emit_u32(dyns.len() as u32);
|
||||
self.emit_u32(total as u32);
|
||||
|
||||
for (&sym, &(val, span)) in stcs.iter() {
|
||||
self.emit_u8(KEY_STATIC);
|
||||
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);
|
||||
self.emit_u32(span_id);
|
||||
}
|
||||
|
||||
for &(_key, val, span) in dyns.iter() {
|
||||
self.emit_maybe_thunk(val);
|
||||
for &(key, val, span) in dyns.iter() {
|
||||
self.emit_u8(KEY_DYNAMIC);
|
||||
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);
|
||||
self.emit_u32(span_id);
|
||||
}
|
||||
@@ -694,102 +869,69 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
|
||||
span: TextRange,
|
||||
) {
|
||||
self.emit_expr(expr);
|
||||
|
||||
let mut dynamic_patches = Vec::new();
|
||||
for attr in attrpath.iter() {
|
||||
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() {
|
||||
match *attr {
|
||||
Attr::Str(sym, _) => {
|
||||
let span_id = self.ctx.register_span(span);
|
||||
self.emit_op(Op::SelectStatic);
|
||||
self.emit_u32(span_id);
|
||||
self.emit_u8(KEY_STATIC);
|
||||
self.emit_str_id(sym);
|
||||
}
|
||||
Attr::Dynamic(key_expr, _) => {
|
||||
self.emit_op(Op::JumpIfSelectFailed);
|
||||
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);
|
||||
Attr::Dynamic(_, _) => {
|
||||
self.emit_u8(KEY_DYNAMIC);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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<'_>>]) {
|
||||
self.emit_expr(lhs);
|
||||
|
||||
let mut dynamic_patches = Vec::new();
|
||||
let [attrs @ .., last] = rhs else {
|
||||
panic!("attrpath is empty");
|
||||
};
|
||||
for attr in attrs {
|
||||
for attr in rhs.iter() {
|
||||
if let Attr::Dynamic(expr, _) = *attr {
|
||||
self.emit_expr(expr);
|
||||
}
|
||||
}
|
||||
self.emit_op(Op::HasAttr);
|
||||
self.emit_u16(rhs.len() as u16);
|
||||
for attr in rhs.iter() {
|
||||
match *attr {
|
||||
Attr::Str(sym, span) => {
|
||||
let span_id = self.ctx.register_span(span);
|
||||
self.emit_op(Op::HasAttrPathStatic);
|
||||
self.emit_u32(span_id);
|
||||
Attr::Str(sym, _) => {
|
||||
self.emit_u8(KEY_STATIC);
|
||||
self.emit_str_id(sym);
|
||||
}
|
||||
Attr::Dynamic(key_expr, span) => {
|
||||
self.emit_op(Op::JumpIfSelectFailed);
|
||||
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);
|
||||
Attr::Dynamic(_, _) => {
|
||||
self.emit_u8(KEY_DYNAMIC);
|
||||
}
|
||||
}
|
||||
}
|
||||
match *last {
|
||||
Attr::Str(sym, _) => {
|
||||
self.emit_op(Op::HasAttrStatic);
|
||||
self.emit_str_id(sym);
|
||||
}
|
||||
Attr::Dynamic(key_expr, _) => {
|
||||
self.emit_op(Op::JumpIfSelectFailed);
|
||||
dynamic_patches.push(self.emit_i32_placeholder());
|
||||
self.emit_expr(key_expr);
|
||||
self.emit_op(Op::HasAttrDynamic);
|
||||
}
|
||||
}
|
||||
for patch in dynamic_patches {
|
||||
self.patch_jump_target(patch);
|
||||
}
|
||||
self.emit_op(Op::HasAttrResolve);
|
||||
}
|
||||
|
||||
fn emit_with(
|
||||
&mut self,
|
||||
namespace: RawIrRef<'_>,
|
||||
body: RawIrRef<'_>,
|
||||
thunks: &[(ThunkId, RawIrRef<'_>)],
|
||||
) {
|
||||
self.emit_expr(namespace);
|
||||
self.emit_op(Op::PushWith);
|
||||
self.emit_scope_thunks(thunks);
|
||||
self.emit_expr(body);
|
||||
self.emit_op(Op::PopWith);
|
||||
}
|
||||
|
||||
fn emit_toplevel_inner(&mut self, body: RawIrRef<'_>, thunks: &[(ThunkId, RawIrRef<'_>)]) {
|
||||
@@ -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 fix_error::{Error, Result, Source};
|
||||
use fix_lang::{BuiltinId, Symbol};
|
||||
use hashbrown::HashSet;
|
||||
use hashbrown::hash_map::Entry;
|
||||
use rnix::TextRange;
|
||||
use rnix::ast::{self, AstToken, Expr, HasEntry};
|
||||
use rnix::ast::{self, Expr, HasEntry};
|
||||
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> {
|
||||
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> {
|
||||
fn new_expr(&self, expr: Ir<'ir, GhostRoRef<'id, 'ir>>) -> GhostRoIrRef<'id, 'ir>;
|
||||
fn maybe_thunk(&mut self, ir: GhostRoIrRef<'id, 'ir>) -> GhostRoMaybeThunkRef<'id, 'ir>;
|
||||
trait BoxIn: Sized {
|
||||
fn box_in(self, bump: &bumpalo::Bump) -> Box<'_, Self> {
|
||||
Box::new_in(self, bump)
|
||||
}
|
||||
}
|
||||
impl<T: Sized> BoxIn for T {}
|
||||
|
||||
fn intern_string(&mut self, sym: impl AsRef<str>) -> StringId;
|
||||
fn resolve_sym(&self, id: StringId) -> Symbol<'_>;
|
||||
fn lookup(&mut self, sym: StringId, span: TextRange) -> Result<GhostRoMaybeThunkRef<'id, 'ir>>;
|
||||
pub trait DowngradeContext<'id: 'ir, 'ir> {
|
||||
fn new_expr(&self, expr: Ir<'ir, IrRef<'id, 'ir>>) -> IrRef<'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 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
|
||||
F: FnOnce(&mut Self) -> R;
|
||||
fn with_let_scope<F, R>(&mut self, bindings: &[StringId], f: F) -> Result<R>
|
||||
where
|
||||
F: FnOnce(&mut Self) -> Result<(Vec<'ir, GhostRoMaybeThunkRef<'id, 'ir>>, R)>;
|
||||
fn with_with_scope<F, R>(&mut self, namespace: GhostRoMaybeThunkRef<'id, 'ir>, f: F) -> R
|
||||
F: FnOnce(&mut Self) -> Result<(Vec<'ir, IrRef<'id, 'ir>>, R)>;
|
||||
fn with_with_scope<F, R>(&mut self, f: F) -> R
|
||||
where
|
||||
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
|
||||
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>> {
|
||||
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 {
|
||||
fn downgrade(self, ctx: &mut Ctx) -> Result<GhostRoIrRef<'id, 'ir>> {
|
||||
fn downgrade(self, ctx: &mut Ctx) -> Result<IrRef<'id, 'ir>> {
|
||||
use Expr::*;
|
||||
match self {
|
||||
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),
|
||||
UnaryOp(op) => op.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),
|
||||
HasAttr(has) => has.downgrade(ctx),
|
||||
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 {
|
||||
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 assertion = self.condition().require(ctx, span)?;
|
||||
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 {
|
||||
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 cond = self.condition().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 {
|
||||
($ty:ident) => {
|
||||
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>> {
|
||||
downgrade_path(self.parts(), ctx)
|
||||
fn downgrade(self, ctx: &mut Ctx) -> Result<IrRef<'id, 'ir>> {
|
||||
let span = self.syntax().text_range();
|
||||
downgrade_path(self.parts(), span, ctx)
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -151,24 +162,19 @@ path!(PathAbs);
|
||||
path!(PathRel);
|
||||
path!(PathHome);
|
||||
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 path = {
|
||||
let temp = self.content().require(ctx, span)?;
|
||||
let text = temp.text();
|
||||
#[expect(
|
||||
clippy::string_slice,
|
||||
reason = "PathSearch text is <...> wrapped in ASCII angle brackets, so byte indices 1 and len-1 are char boundaries"
|
||||
)]
|
||||
let id = ctx.intern_string(&text[1..text.len() - 1]);
|
||||
let expr = ctx.new_expr(Ir::Str(id));
|
||||
ctx.maybe_thunk(expr)
|
||||
ctx.new_expr(Ir::Str(
|
||||
text[1..text.len() - 1].to_string().box_in(ctx.bump()),
|
||||
))
|
||||
};
|
||||
// HACK: disgusting eww
|
||||
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.maybe_thunk(nix_path);
|
||||
let call = ctx.new_expr(Ir::Call {
|
||||
func: find_file,
|
||||
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 {
|
||||
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 is_single_literal = normalized.len() == 1
|
||||
&& matches!(normalized.first(), Some(ast::InterpolPart::Literal(_)));
|
||||
|
||||
let bump = ctx.bump();
|
||||
let mut parts = normalized.into_iter().map(|part| match part {
|
||||
ast::InterpolPart::Literal(lit) => {
|
||||
let id = ctx.intern_string(lit);
|
||||
Ok(ctx.new_expr(Ir::Str(id)))
|
||||
}
|
||||
ast::InterpolPart::Interpolation(interpol) => interpol
|
||||
.expr()
|
||||
.require(ctx, interpol.syntax().text_range())?
|
||||
.downgrade(ctx),
|
||||
});
|
||||
let parts = normalized
|
||||
.into_iter()
|
||||
.map(|part| match part {
|
||||
ast::InterpolPart::Literal(lit) => Ok(ctx.new_expr(Ir::Str(lit.box_in(bump)))),
|
||||
ast::InterpolPart::Interpolation(interpol) => {
|
||||
let inner = interpol
|
||||
.expr()
|
||||
.require(ctx, interpol.syntax().text_range())?
|
||||
.downgrade(ctx)?;
|
||||
Ok(ctx.maybe_thunk(inner))
|
||||
}
|
||||
})
|
||||
.collect_in::<Result<Vec<'ir, _>>>(bump)?;
|
||||
|
||||
Ok(if is_single_literal {
|
||||
parts.next().expect("is_single_literal checked")?
|
||||
parts.into_iter().next().expect("is_single_literal checked")
|
||||
} else {
|
||||
let parts = parts.collect_in::<Result<_>>(bump)?;
|
||||
ctx.new_expr(Ir::ConcatStrings {
|
||||
parts,
|
||||
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 {
|
||||
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 expr = match self.kind() {
|
||||
Ok(ctx.new_expr(match self.kind() {
|
||||
ast::LiteralKind::Integer(int) => Ir::Int(int.value().require(ctx, span)?),
|
||||
ast::LiteralKind::Float(float) => Ir::Float(float.value().require(ctx, span)?),
|
||||
ast::LiteralKind::Uri(uri) => {
|
||||
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()),
|
||||
ast::LiteralKind::Uri(uri) => Ir::Str(uri.to_string().box_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 {
|
||||
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();
|
||||
if !rec {
|
||||
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 {
|
||||
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 items = self
|
||||
.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 {
|
||||
fn downgrade(self, ctx: &mut Ctx) -> Result<GhostRoIrRef<'id, 'ir>> {
|
||||
use BinOpKind::*;
|
||||
use ast::BinOpKind as Kind;
|
||||
|
||||
fn downgrade(self, ctx: &mut Ctx) -> Result<IrRef<'id, 'ir>> {
|
||||
let span = self.syntax().text_range();
|
||||
let lhs = self.lhs().require(ctx, span)?.downgrade(ctx)?;
|
||||
let rhs = self.rhs().require(ctx, span)?.downgrade(ctx)?;
|
||||
let kind = match self.operator().require(ctx, span)? {
|
||||
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,
|
||||
}));
|
||||
}
|
||||
};
|
||||
let kind = self.operator().require(ctx, span)?.into();
|
||||
Ok(ctx.new_expr(Ir::BinOp { lhs, rhs, kind }))
|
||||
}
|
||||
}
|
||||
|
||||
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 lhs = self.expr().require(ctx, span)?.downgrade(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 {
|
||||
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 rhs = self.expr().require(ctx, span)?.downgrade(ctx)?;
|
||||
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 {
|
||||
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 expr = self.expr().require(ctx, span)?.downgrade(ctx)?;
|
||||
let attrpath = downgrade_attrpath(self.attrpath().require(ctx, span)?, ctx)?;
|
||||
let default = self
|
||||
.default_expr()
|
||||
.map(|expr| expr.downgrade(ctx))
|
||||
.transpose()?;
|
||||
let default = if let Some(default) = self.default_expr() {
|
||||
let default_expr = default.downgrade(ctx)?;
|
||||
Some(ctx.maybe_thunk(default_expr))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let span = self.syntax().text_range();
|
||||
Ok(ctx.new_expr(Ir::Select {
|
||||
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 {
|
||||
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 entries: Vec<'ir, _> = self.entries().collect_in(ctx.bump());
|
||||
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 }))
|
||||
})?;
|
||||
|
||||
let body_sym = ctx.intern_string("body");
|
||||
let body_sym = ctx.new_sym("body".to_string());
|
||||
Ok(ctx.new_expr(Ir::Select {
|
||||
expr: attrset_expr,
|
||||
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 {
|
||||
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 span = self.syntax().text_range();
|
||||
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 {
|
||||
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 namespace = self.namespace().require(ctx, span)?.downgrade(ctx)?;
|
||||
let namespace = ctx.maybe_thunk(namespace);
|
||||
|
||||
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 {
|
||||
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 raw_param = self.param().require(ctx, span)?;
|
||||
let body_ast = self.body().require(ctx, span)?;
|
||||
let arg = ctx.new_arg();
|
||||
|
||||
struct Ret<'id, 'ir> {
|
||||
param: Option<Param<'ir>>,
|
||||
body: GhostRoIrRef<'id, 'ir>,
|
||||
body: IrRef<'id, 'ir>,
|
||||
}
|
||||
|
||||
let (ret, thunks) = ctx.with_thunk_scope(|ctx| -> Result<Ret> {
|
||||
let (param, body) = match raw_param {
|
||||
let (ret, thunks) = ctx.with_thunk_scope(|ctx| {
|
||||
let param;
|
||||
let body;
|
||||
|
||||
match raw_param {
|
||||
ast::Param::IdentParam(id) => {
|
||||
let param_sym = ctx.intern_string(id.to_string());
|
||||
(
|
||||
None,
|
||||
ctx.with_param_scope(param_sym, |ctx| body_ast.downgrade(ctx))?,
|
||||
)
|
||||
let param_sym = ctx.new_sym(id.to_string());
|
||||
param = None;
|
||||
|
||||
body = ctx
|
||||
.with_param_scope(param_sym, arg, |ctx| body_ast.clone().downgrade(ctx))?;
|
||||
}
|
||||
ast::Param::Pattern(pattern) => {
|
||||
let alias = pattern
|
||||
.pat_bind()
|
||||
.map(|alias| {
|
||||
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()?;
|
||||
|
||||
@@ -491,35 +417,35 @@ impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> fo
|
||||
body: inner_body,
|
||||
required,
|
||||
optional,
|
||||
} = downgrade_pattern_bindings(pat_entries, alias, ctx, |ctx, _| {
|
||||
} = downgrade_pattern_bindings(pat_entries, alias, arg, ctx, |ctx, _| {
|
||||
body_ast.clone().downgrade(ctx)
|
||||
})?;
|
||||
|
||||
(
|
||||
Some(Param {
|
||||
required,
|
||||
optional,
|
||||
ellipsis,
|
||||
}),
|
||||
inner_body,
|
||||
)
|
||||
}
|
||||
};
|
||||
param = Some(Param {
|
||||
required,
|
||||
optional,
|
||||
ellipsis,
|
||||
});
|
||||
|
||||
Ok(Ret { param, body })
|
||||
body = inner_body;
|
||||
}
|
||||
}
|
||||
|
||||
Result::Ok(Ret { param, body })
|
||||
});
|
||||
let Ret { param, body } = ret?;
|
||||
|
||||
Ok(ctx.new_expr(Ir::Func {
|
||||
body,
|
||||
param,
|
||||
arg,
|
||||
thunks,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
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 func = self.lambda().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(
|
||||
&mut self,
|
||||
path: &[ast::Attr],
|
||||
@@ -569,7 +491,7 @@ impl<'id: 'ir, 'ir> PendingAttrSet<'ir> {
|
||||
|
||||
match first {
|
||||
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)
|
||||
}
|
||||
ast::Attr::Str(string) => {
|
||||
@@ -578,7 +500,7 @@ impl<'id: 'ir, 'ir> PendingAttrSet<'ir> {
|
||||
&& let ast::InterpolPart::Literal(lit) =
|
||||
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);
|
||||
}
|
||||
self.insert_dynamic(first.clone(), span, rest, value, ctx)
|
||||
@@ -658,10 +580,6 @@ impl<'id: 'ir, 'ir> PendingAttrSet<'ir> {
|
||||
) -> Result<()> {
|
||||
if !path.is_empty() {
|
||||
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(
|
||||
path[0].clone(),
|
||||
path[0].syntax().text_range(),
|
||||
@@ -676,10 +594,6 @@ impl<'id: 'ir, 'ir> PendingAttrSet<'ir> {
|
||||
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>(
|
||||
value: &'a mut PendingValue<'ir>,
|
||||
ctx: &mut impl DowngradeContext<'id, 'ir>,
|
||||
@@ -829,14 +743,14 @@ impl<'id: 'ir, 'ir> PendingAttrSet<'ir> {
|
||||
for attr in inherit.attrs() {
|
||||
let span = attr.syntax().text_range();
|
||||
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) => {
|
||||
let parts = s.normalized_parts();
|
||||
if parts.len() == 1
|
||||
&& let ast::InterpolPart::Literal(lit) =
|
||||
parts.into_iter().next().expect("len checked")
|
||||
{
|
||||
ctx.intern_string(lit)
|
||||
ctx.new_sym(lit)
|
||||
} else {
|
||||
return Err(Error::downgrade_error(
|
||||
"dynamic attributes not allowed in inherit".to_string(),
|
||||
@@ -856,7 +770,7 @@ impl<'id: 'ir, 'ir> PendingAttrSet<'ir> {
|
||||
|
||||
if self.stcs.contains_key(&sym) {
|
||||
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(),
|
||||
span,
|
||||
));
|
||||
@@ -933,15 +847,8 @@ fn make_attrpath_value_entry<'ir>(path: Vec<'ir, ast::Attr>, value: ast::Expr) -
|
||||
}
|
||||
|
||||
struct FinalizedAttrSet<'id, 'ir> {
|
||||
stcs: HashMap<'ir, StringId, (GhostRoMaybeThunkRef<'id, 'ir>, TextRange)>,
|
||||
dyns: Vec<
|
||||
'ir,
|
||||
(
|
||||
GhostRoIrRef<'id, 'ir>,
|
||||
GhostRoMaybeThunkRef<'id, 'ir>,
|
||||
TextRange,
|
||||
),
|
||||
>,
|
||||
stcs: HashMap<'ir, StringId, (IrRef<'id, 'ir>, TextRange)>,
|
||||
dyns: Vec<'ir, (IrRef<'id, 'ir>, IrRef<'id, 'ir>, TextRange)>,
|
||||
}
|
||||
|
||||
fn downgrade_attrs<'id, 'ir>(
|
||||
@@ -956,22 +863,22 @@ fn downgrade_attrs<'id, 'ir>(
|
||||
fn downgrade_attr<'id, 'ir>(
|
||||
attr: ast::Attr,
|
||||
ctx: &mut impl DowngradeContext<'id, 'ir>,
|
||||
) -> Result<Attr<GhostRoIrRef<'id, 'ir>>> {
|
||||
) -> Result<Attr<IrRef<'id, 'ir>>> {
|
||||
use ast::Attr::*;
|
||||
use ast::InterpolPart::*;
|
||||
match attr {
|
||||
Ident(ident) => Ok(Attr::Str(
|
||||
ctx.intern_string(ident.to_string()),
|
||||
ctx.new_sym(ident.to_string()),
|
||||
ident.syntax().text_range(),
|
||||
)),
|
||||
Str(string) => {
|
||||
let parts = string.normalized_parts();
|
||||
let span = string.syntax().text_range();
|
||||
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 {
|
||||
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(
|
||||
interpol
|
||||
.expr()
|
||||
@@ -985,10 +892,7 @@ fn downgrade_attr<'id, 'ir>(
|
||||
let parts = parts
|
||||
.into_iter()
|
||||
.map(|part| match part {
|
||||
Literal(lit) => {
|
||||
let id = ctx.intern_string(lit);
|
||||
Ok(ctx.new_expr(Ir::Str(id)))
|
||||
}
|
||||
Literal(lit) => Ok(ctx.new_expr(Ir::Str(lit.box_in(bump)))),
|
||||
Interpolation(interpol) => interpol
|
||||
.expr()
|
||||
.require(ctx, interpol.syntax().text_range())?
|
||||
@@ -1017,7 +921,7 @@ fn downgrade_attr<'id, 'ir>(
|
||||
fn downgrade_attrpath<'id, 'ir>(
|
||||
attrpath: ast::Attrpath,
|
||||
ctx: &mut impl DowngradeContext<'id, 'ir>,
|
||||
) -> Result<Vec<'ir, Attr<GhostRoIrRef<'id, 'ir>>>> {
|
||||
) -> Result<Vec<'ir, Attr<IrRef<'id, 'ir>>>> {
|
||||
let bump = ctx.bump();
|
||||
attrpath
|
||||
.attrs()
|
||||
@@ -1026,7 +930,7 @@ fn downgrade_attrpath<'id, 'ir>(
|
||||
}
|
||||
|
||||
struct PatternBindings<'id, 'ir> {
|
||||
body: GhostRoIrRef<'id, 'ir>,
|
||||
body: IrRef<'id, 'ir>,
|
||||
required: Vec<'ir, (StringId, TextRange)>,
|
||||
optional: Vec<'ir, (StringId, TextRange)>,
|
||||
}
|
||||
@@ -1034,12 +938,14 @@ struct PatternBindings<'id, 'ir> {
|
||||
fn downgrade_pattern_bindings<'id, 'ir, Ctx>(
|
||||
pat_entries: impl Iterator<Item = ast::PatEntry>,
|
||||
alias: Option<StringId>,
|
||||
arg: ArgId,
|
||||
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>>
|
||||
where
|
||||
Ctx: DowngradeContext<'id, 'ir>,
|
||||
{
|
||||
let arg = ctx.new_expr(Ir::Arg(arg));
|
||||
struct Param {
|
||||
sym: StringId,
|
||||
sym_span: TextRange,
|
||||
@@ -1053,13 +959,13 @@ where
|
||||
for entry in pat_entries {
|
||||
let ident = entry.ident().require(ctx, entry.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 span = entry.syntax().text_range();
|
||||
|
||||
if !seen_params.insert(sym) {
|
||||
return Err(Error::downgrade_error(
|
||||
format!("duplicate parameter '{}'", ctx.resolve_sym(sym)),
|
||||
format!("duplicate parameter '{}'", ctx.get_sym(sym)),
|
||||
ctx.get_current_source(),
|
||||
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| {
|
||||
let vals = params
|
||||
.into_iter()
|
||||
@@ -1105,17 +1009,21 @@ where
|
||||
default,
|
||||
span,
|
||||
} = 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,
|
||||
attrpath: Vec::from_iter_in([Attr::Str(sym, sym_span)], bump),
|
||||
default,
|
||||
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)?;
|
||||
|
||||
let body = body_fn(ctx, &keys)?;
|
||||
@@ -1135,10 +1043,10 @@ fn downgrade_let_bindings<'id, 'ir, Ctx, F>(
|
||||
entries: Vec<'ir, ast::Entry>,
|
||||
ctx: &mut Ctx,
|
||||
body_fn: F,
|
||||
) -> Result<GhostRoIrRef<'id, 'ir>>
|
||||
) -> Result<IrRef<'id, 'ir>>
|
||||
where
|
||||
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| {
|
||||
body_fn(ctx, binding_keys)
|
||||
@@ -1148,7 +1056,7 @@ where
|
||||
fn downgrade_rec_bindings<'id, 'ir, Ctx>(
|
||||
entries: Vec<'ir, ast::Entry>,
|
||||
ctx: &mut Ctx,
|
||||
) -> Result<GhostRoIrRef<'id, 'ir>>
|
||||
) -> Result<IrRef<'id, 'ir>>
|
||||
where
|
||||
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>,
|
||||
ctx: &mut Ctx,
|
||||
body_fn: F,
|
||||
) -> Result<GhostRoIrRef<'id, 'ir>>
|
||||
) -> Result<IrRef<'id, 'ir>>
|
||||
where
|
||||
Ctx: DowngradeContext<'id, 'ir>,
|
||||
F: FnOnce(
|
||||
&mut Ctx,
|
||||
&[StringId],
|
||||
&[(
|
||||
GhostRoIrRef<'id, 'ir>,
|
||||
GhostRoMaybeThunkRef<'id, 'ir>,
|
||||
TextRange,
|
||||
)],
|
||||
) -> Result<GhostRoIrRef<'id, 'ir>>,
|
||||
&[(IrRef<'id, 'ir>, IrRef<'id, 'ir>, TextRange)],
|
||||
) -> Result<IrRef<'id, 'ir>>,
|
||||
{
|
||||
let mut pending = PendingAttrSet::new_in(ctx.bump());
|
||||
pending.collect_entries(entries.iter().cloned(), ctx)?;
|
||||
@@ -1192,21 +1096,18 @@ where
|
||||
|
||||
ctx.with_let_scope(&keys, |ctx| {
|
||||
let finalized = finalize_pending_set::<_, ALLOW_DYN>(pending, &inherit_lookups, ctx)?;
|
||||
let vals = {
|
||||
let mut temp = Vec::with_capacity_in(keys.len(), ctx.bump());
|
||||
for sym in &keys {
|
||||
temp.push(finalized.stcs.get(sym).expect("WTF").0);
|
||||
}
|
||||
temp
|
||||
};
|
||||
let vals = keys
|
||||
.iter()
|
||||
.map(|sym| finalized.stcs.get(sym).expect("WTF").0)
|
||||
.collect_in(ctx.bump());
|
||||
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],
|
||||
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());
|
||||
for entry in entries {
|
||||
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() {
|
||||
if let ast::Attr::Ident(ident) = attr {
|
||||
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)?;
|
||||
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 {
|
||||
if !binding_syms.insert(*sym) {
|
||||
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(),
|
||||
*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>(
|
||||
pending: PendingAttrSet,
|
||||
inherit_lookups: &HashMap<StringId, (GhostRoMaybeThunkRef<'id, 'ir>, TextRange)>,
|
||||
inherit_lookups: &HashMap<StringId, (IrRef<'id, 'ir>, TextRange)>,
|
||||
ctx: &mut Ctx,
|
||||
) -> Result<FinalizedAttrSet<'id, 'ir>> {
|
||||
let mut stcs = HashMap::new_in(ctx.bump());
|
||||
let mut dyns = Vec::new_in(ctx.bump());
|
||||
|
||||
for (sym, (value, value_span)) in pending.stcs {
|
||||
let expr = finalize_pending_value::<_, ALLOW_DYN>(value, inherit_lookups, ctx)?;
|
||||
stcs.insert(sym, (ctx.maybe_thunk(expr), value_span));
|
||||
let expr_id = finalize_pending_value::<_, ALLOW_DYN>(value, inherit_lookups, ctx)?;
|
||||
stcs.insert(sym, (expr_id, value_span));
|
||||
}
|
||||
|
||||
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_expr = match key_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)?;
|
||||
dyns.push((key_expr, ctx.maybe_thunk(value), value_span));
|
||||
let value_id = finalize_pending_value::<_, ALLOW_DYN>(value, inherit_lookups, ctx)?;
|
||||
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>(
|
||||
value: PendingValue,
|
||||
inherit_lookups: &HashMap<StringId, (GhostRoMaybeThunkRef<'id, 'ir>, TextRange)>,
|
||||
inherit_lookups: &HashMap<StringId, (IrRef<'id, 'ir>, TextRange)>,
|
||||
ctx: &mut Ctx,
|
||||
) -> Result<GhostRoIrRef<'id, 'ir>> {
|
||||
) -> Result<IrRef<'id, 'ir>> {
|
||||
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) => {
|
||||
let from = Downgrade::downgrade(from_expr, ctx)?;
|
||||
Ok(ctx.new_expr(Ir::Select {
|
||||
expr: from,
|
||||
let from_id = Downgrade::downgrade(from_expr, ctx)?;
|
||||
let select_id = ctx.new_expr(Ir::Select {
|
||||
expr: from_id,
|
||||
attrpath: Vec::from_iter_in([Attr::Str(sym, span)], ctx.bump()),
|
||||
default: None,
|
||||
span,
|
||||
}))
|
||||
});
|
||||
Ok(ctx.maybe_thunk(select_id))
|
||||
}
|
||||
PendingValue::InheritScope(sym, span) => {
|
||||
if let Some(&(expr, _)) = inherit_lookups.get(&sym) {
|
||||
Ok(ctx.new_expr(Ir::MaybeThunk(expr)))
|
||||
Ok(ctx.maybe_thunk(expr))
|
||||
} else {
|
||||
ctx.lookup(sym, span)
|
||||
.map(|val| ctx.new_expr(Ir::MaybeThunk(val)))
|
||||
let lookup_id = ctx.lookup(sym, span)?;
|
||||
Ok(ctx.maybe_thunk(lookup_id))
|
||||
}
|
||||
}
|
||||
PendingValue::Set(set) => {
|
||||
@@ -1315,15 +1222,15 @@ fn finalize_pending_value<'id, 'ir, Ctx: DowngradeContext<'id, 'ir>, const ALLOW
|
||||
|
||||
fn downgrade_path<'id, 'ir>(
|
||||
parts: impl IntoIterator<Item = ast::InterpolPart<ast::PathContent>>,
|
||||
_span: rnix::TextRange,
|
||||
ctx: &mut impl DowngradeContext<'id, 'ir>,
|
||||
) -> Result<GhostRoIrRef<'id, 'ir>> {
|
||||
) -> Result<IrRef<'id, 'ir>> {
|
||||
let bump = ctx.bump();
|
||||
let parts = parts
|
||||
.into_iter()
|
||||
.map(|part| match part {
|
||||
ast::InterpolPart::Literal(lit) => {
|
||||
let id = ctx.intern_string(lit.text());
|
||||
Ok(ctx.new_expr(Ir::Str(id)))
|
||||
Ok(ctx.new_expr(Ir::Str(lit.text().to_string().box_in(ctx.bump()))))
|
||||
}
|
||||
ast::InterpolPart::Interpolation(interpol) => interpol
|
||||
.expr()
|
||||
+693
@@ -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
@@ -1,149 +1,22 @@
|
||||
use fix_bytecode::InstructionPtr;
|
||||
use fix_bytecode::disassembler::{Disassembler, DisassemblerContext};
|
||||
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 _};
|
||||
#![warn(clippy::unwrap_used)]
|
||||
#![allow(dead_code)]
|
||||
|
||||
mod boxing;
|
||||
pub mod error;
|
||||
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]
|
||||
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
@@ -5,7 +5,7 @@ use tracing_subscriber::layer::SubscriberExt;
|
||||
use tracing_subscriber::util::SubscriberInitExt;
|
||||
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 show_time = env::var("NIX_JS_LOG_TIME")
|
||||
.map(|v| v == "1" || v.to_lowercase() == "true")
|
||||
@@ -32,10 +32,10 @@ pub fn init_logging() -> Result<(), miette::InstallError> {
|
||||
.with(fmt_layer)
|
||||
.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();
|
||||
miette::set_hook(Box::new(move |_| {
|
||||
Box::new(
|
||||
@@ -46,4 +46,5 @@ fn init_miette_handler() -> Result<(), miette::InstallError> {
|
||||
.build(),
|
||||
)
|
||||
}))
|
||||
.ok();
|
||||
}
|
||||
|
||||
+33
-48
@@ -3,8 +3,8 @@ use std::process::exit;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{Args, Parser, Subcommand};
|
||||
use fix::Evaluator;
|
||||
use fix_error::Source;
|
||||
use fix::error::Source;
|
||||
use fix::runtime::Runtime;
|
||||
use hashbrown::HashSet;
|
||||
use rustyline::DefaultEditor;
|
||||
use rustyline::error::ReadlineError;
|
||||
@@ -20,54 +20,38 @@ struct Cli {
|
||||
enum Command {
|
||||
Compile {
|
||||
#[clap(flatten)]
|
||||
source: ExprSourceArgs,
|
||||
source: ExprSource,
|
||||
#[arg(long)]
|
||||
silent: bool,
|
||||
},
|
||||
Eval {
|
||||
#[clap(flatten)]
|
||||
source: ExprSourceArgs,
|
||||
source: ExprSource,
|
||||
},
|
||||
Repl,
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
#[group(required = true, multiple = false)]
|
||||
struct ExprSourceArgs {
|
||||
struct ExprSource {
|
||||
#[clap(short, long)]
|
||||
expr: Option<String>,
|
||||
#[clap(short, long)]
|
||||
file: Option<PathBuf>,
|
||||
}
|
||||
|
||||
enum ExprSource {
|
||||
Expr(String),
|
||||
File(PathBuf),
|
||||
}
|
||||
|
||||
#[expect(
|
||||
clippy::unreachable,
|
||||
reason = "clap's arg group guarantees exactly one of --expr/--file is set"
|
||||
)]
|
||||
impl From<ExprSourceArgs> for ExprSource {
|
||||
fn from(args: ExprSourceArgs) -> Self {
|
||||
match (args.expr, args.file) {
|
||||
(Some(expr), None) => ExprSource::Expr(expr),
|
||||
(None, Some(file)) => ExprSource::File(file),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn run_compile(eval: &mut Evaluator, src: ExprSource, silent: bool) -> Result<()> {
|
||||
let src = match src {
|
||||
ExprSource::Expr(expr) => Source::new_eval(expr)?,
|
||||
ExprSource::File(file) => Source::new_file(file)?,
|
||||
fn run_compile(runtime: &mut Runtime, src: ExprSource, silent: bool) -> Result<()> {
|
||||
let src = if let Some(expr) = src.expr {
|
||||
Source::new_eval(expr)?
|
||||
} else if let Some(file) = src.file {
|
||||
Source::new_file(file)?
|
||||
} else {
|
||||
unreachable!()
|
||||
};
|
||||
match eval.compile_bytecode(src) {
|
||||
match runtime.compile_bytecode(src) {
|
||||
Ok(ip) => {
|
||||
if !silent {
|
||||
println!("{}", eval.disassemble_colored(ip));
|
||||
println!("{}", runtime.disassemble_colored(ip));
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
@@ -78,12 +62,15 @@ fn run_compile(eval: &mut Evaluator, src: ExprSource, silent: bool) -> Result<()
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_eval(eval: &mut Evaluator, src: ExprSource) -> Result<()> {
|
||||
let src = match src {
|
||||
ExprSource::Expr(expr) => Source::new_eval(expr)?,
|
||||
ExprSource::File(file) => Source::new_file(file)?,
|
||||
fn run_eval(runtime: &mut Runtime, src: ExprSource) -> Result<()> {
|
||||
let src = if let Some(expr) = src.expr {
|
||||
Source::new_eval(expr)?
|
||||
} 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) => {
|
||||
println!("{}", value.display_compat());
|
||||
}
|
||||
@@ -95,20 +82,18 @@ fn run_eval(eval: &mut Evaluator, src: ExprSource) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_repl(eval: &mut Evaluator) -> Result<()> {
|
||||
fn run_repl(runtime: &mut Runtime) -> Result<()> {
|
||||
let mut rl = DefaultEditor::new()?;
|
||||
let mut scope = HashSet::new();
|
||||
const RE: ere::Regex<3> = ere::compile_regex!("^[ \t]*([a-zA-Z_][a-zA-Z0-9_'-]*)[ \t]*(.*)$");
|
||||
loop {
|
||||
let readline = rl.readline("fix-repl> ");
|
||||
let readline = rl.readline("nix-js-repl> ");
|
||||
match readline {
|
||||
Ok(line) => {
|
||||
if line.trim().is_empty() {
|
||||
continue;
|
||||
}
|
||||
if let Err(err) = rl.add_history_entry(line.as_str()) {
|
||||
eprintln!("[WARN] Failed to add history entry: {err}");
|
||||
}
|
||||
let _ = rl.add_history_entry(line.as_str());
|
||||
if let Some([Some(_), Some(ident), Some(rest)]) = RE.exec(&line) {
|
||||
if let Some(expr) = rest.strip_prefix('=') {
|
||||
let expr = expr.trim_start();
|
||||
@@ -116,20 +101,20 @@ fn run_repl(eval: &mut Evaluator) -> Result<()> {
|
||||
eprintln!("Error: missing expression after '='");
|
||||
continue;
|
||||
}
|
||||
match eval.add_binding(ident, expr, &mut scope) {
|
||||
match runtime.add_binding(ident, expr, &mut scope) {
|
||||
Ok(value) => println!("{} = {}", ident, value),
|
||||
Err(err) => eprintln!("{:?}", miette::Report::new(*err)),
|
||||
}
|
||||
} else {
|
||||
let src = Source::new_repl(line)?;
|
||||
match eval.eval_repl(src, &scope) {
|
||||
match runtime.eval_repl(src, &scope) {
|
||||
Ok(value) => println!("{value}"),
|
||||
Err(err) => eprintln!("{:?}", miette::Report::new(*err)),
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let src = Source::new_repl(line)?;
|
||||
match eval.eval_repl(src, &scope) {
|
||||
match runtime.eval_repl(src, &scope) {
|
||||
Ok(value) => println!("{value}"),
|
||||
Err(err) => eprintln!("{:?}", miette::Report::new(*err)),
|
||||
}
|
||||
@@ -152,15 +137,15 @@ fn run_repl(eval: &mut Evaluator) -> Result<()> {
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
fix::logging::init_logging()?;
|
||||
fix::logging::init_logging();
|
||||
|
||||
let cli = Cli::parse();
|
||||
|
||||
let mut eval = Evaluator::new();
|
||||
let mut runtime = Runtime::new()?;
|
||||
|
||||
match cli.command {
|
||||
Command::Compile { source, silent } => run_compile(&mut eval, source.into(), silent),
|
||||
Command::Eval { source } => run_eval(&mut eval, source.into()),
|
||||
Command::Repl => run_repl(&mut eval),
|
||||
Command::Compile { source, silent } => run_compile(&mut runtime, source, silent),
|
||||
Command::Eval { source } => run_eval(&mut runtime, source),
|
||||
Command::Repl => run_repl(&mut runtime),
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ use std::path::Path;
|
||||
use nix_nar::Encoder;
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
use fix_error::{Error, Result};
|
||||
use crate::error::{Error, Result};
|
||||
|
||||
pub fn compute_nar_hash(path: &Path) -> Result<[u8; 32]> {
|
||||
let mut hasher = Sha256::new();
|
||||
|
||||
@@ -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:
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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
|
||||
@@ -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; })
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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(¤t_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(¤t_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))
|
||||
}
|
||||
@@ -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::collections::BTreeMap;
|
||||
use std::fmt::{Debug, Display, Formatter, Result as FmtResult};
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::ops::DerefMut;
|
||||
|
||||
use gc_arena::Collect;
|
||||
use num_enum::TryFromPrimitive;
|
||||
|
||||
macro_rules! define_builtins {
|
||||
($(($name:literal, $variant:ident, $arity:expr)),* $(,)?) => {
|
||||
const BUILTINS: &[(&str, u8)] = &[
|
||||
$(($name, $arity),)*
|
||||
];
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, TryFromPrimitive, Collect)]
|
||||
#[repr(u8)]
|
||||
#[collect(require_static)]
|
||||
pub enum BuiltinId {
|
||||
$($variant,)*
|
||||
}
|
||||
|
||||
impl BuiltinId {
|
||||
pub const ALL: [Self; BUILTINS.len()] = [$(Self::$variant,)*];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
define_builtins! {
|
||||
("abort", Abort, 1),
|
||||
("__add", Add, 2),
|
||||
("__addErrorContext", AddErrorContext, 2),
|
||||
("__all", All, 2),
|
||||
("__any", Any, 2),
|
||||
("__appendContext", AppendContext, 2),
|
||||
("__attrNames", AttrNames, 1),
|
||||
("__attrValues", AttrValues, 1),
|
||||
("baseNameOf", BaseNameOf, 1),
|
||||
("__bitAnd", BitAnd, 2),
|
||||
("__bitOr", BitOr, 2),
|
||||
("__bitXor", BitXor, 2),
|
||||
("break", Break, 1),
|
||||
("__catAttrs", CatAttrs, 2),
|
||||
("__ceil", Ceil, 1),
|
||||
("__compareVersions", CompareVersions, 2),
|
||||
("__concatLists", ConcatLists, 1),
|
||||
("__concatMap", ConcatMap, 2),
|
||||
("__concatStringsSep", ConcatStringsSep, 2),
|
||||
("__convertHash", ConvertHash, 1),
|
||||
("__deepSeq", DeepSeq, 2),
|
||||
("derivation", Derivation, 1),
|
||||
("derivationStrict", DerivationStrict, 1),
|
||||
("dirOf", DirOf, 1),
|
||||
("__div", Div, 2),
|
||||
("__elem", Elem, 2),
|
||||
("__elemAt", ElemAt, 2),
|
||||
("fetchGit", FetchGit, 1),
|
||||
("fetchMercurial", FetchMercurial, 1),
|
||||
("fetchTarball", FetchTarball, 1),
|
||||
("fetchTree", FetchTree, 1),
|
||||
("__fetchurl", FetchUrl, 1),
|
||||
("__filter", Filter, 2),
|
||||
("__filterSource", FilterSource, 2),
|
||||
("__findFile", FindFile, 2),
|
||||
("__floor", Floor, 1),
|
||||
("__foldl'", FoldlStrict, 3),
|
||||
("__fromJSON", FromJSON, 1),
|
||||
("fromTOML", FromTOML, 1),
|
||||
("__functionArgs", FunctionArgs, 1),
|
||||
("__genList", GenList, 2),
|
||||
("__genericClosure", GenericClosure, 1),
|
||||
("__getAttr", GetAttr, 2),
|
||||
("__getContext", GetContext, 1),
|
||||
("__getEnv", GetEnv, 1),
|
||||
("__groupBy", GroupBy, 2),
|
||||
("__hasAttr", HasAttr, 2),
|
||||
("__hasContext", HasContext, 1),
|
||||
("__hashFile", HashFile, 2),
|
||||
("__hashString", HashString, 2),
|
||||
("__head", Head, 1),
|
||||
("import", Import, 1),
|
||||
("__intersectAttrs", IntersectAttrs, 2),
|
||||
("__isAttrs", IsAttrs, 1),
|
||||
("__isBool", IsBool, 1),
|
||||
("__isFloat", IsFloat, 1),
|
||||
("__isFunction", IsFunction, 1),
|
||||
("__isInt", IsInt, 1),
|
||||
("__isList", IsList, 1),
|
||||
("isNull", IsNull, 1),
|
||||
("__isPath", IsPath, 1),
|
||||
("__isString", IsString, 1),
|
||||
("__length", Length, 1),
|
||||
("__lessThan", LessThan, 2),
|
||||
("__listToAttrs", ListToAttrs, 1),
|
||||
("map", Map, 2),
|
||||
("__mapAttrs", MapAttrs, 2),
|
||||
("__match", Match, 2),
|
||||
("__mul", Mul, 2),
|
||||
("__parseDrvName", ParseDrvName, 1),
|
||||
("__partition", Partition, 2),
|
||||
("__path", Path, 1),
|
||||
("__pathExists", PathExists, 1),
|
||||
("placeholder", Placeholder, 1),
|
||||
("__readDir", ReadDir, 1),
|
||||
("__readFile", ReadFile, 1),
|
||||
("__readFileType", ReadFileType, 1),
|
||||
("removeAttrs", RemoveAttrs, 2),
|
||||
("__replaceStrings", ReplaceStrings, 3),
|
||||
("scopedImport", ScopedImport, 2),
|
||||
("__seq", Seq, 2),
|
||||
("__sort", Sort, 2),
|
||||
("__split", Split, 2),
|
||||
("__splitVersion", SplitVersion, 1),
|
||||
("__storePath", StorePath, 1),
|
||||
("__stringLength", StringLength, 1),
|
||||
("__sub", Sub, 2),
|
||||
("__substring", Substring, 3),
|
||||
("__tail", Tail, 1),
|
||||
("throw", Throw, 1),
|
||||
("__toFile", ToFile, 2),
|
||||
("__toJSON", ToJSON, 1),
|
||||
("__toPath", ToPath, 1),
|
||||
("toString", ToString, 1),
|
||||
("__toXML", ToXML, 1),
|
||||
("__trace", Trace, 2),
|
||||
("__tryEval", TryEval, 1),
|
||||
("__typeOf", TypeOf, 1),
|
||||
("__unsafeDiscardStringContext", UnsafeDiscardStringContext, 1),
|
||||
("__unsafeDiscardOutputDependency", UnsafeDiscardOutputDependency, 1),
|
||||
("__unsafeGetAttrPos", UnsafeGetAttrPos, 2),
|
||||
("__warn", Warn, 2),
|
||||
("__zipAttrsWith", ZipAttrsWith, 2),
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct BuiltinInfo {
|
||||
pub name: &'static str,
|
||||
pub global_name: &'static str,
|
||||
pub global: bool,
|
||||
pub arity: u8,
|
||||
}
|
||||
|
||||
impl BuiltinId {
|
||||
pub const TOTAL: usize = BUILTINS.len();
|
||||
|
||||
#[expect(
|
||||
clippy::indexing_slicing,
|
||||
reason = "a `BuiltinId` discriminant is always a valid index into the `BUILTINS` table"
|
||||
)]
|
||||
#[inline(always)]
|
||||
pub fn info(self) -> BuiltinInfo {
|
||||
let (global_name, arity) = BUILTINS[self as usize];
|
||||
let (name, global) = global_name
|
||||
.strip_prefix("__")
|
||||
.map_or((global_name, true), |name| (name, false));
|
||||
BuiltinInfo {
|
||||
name,
|
||||
global_name,
|
||||
global,
|
||||
arity,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(transparent)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Collect)]
|
||||
#[collect(require_static)]
|
||||
pub struct StringId(pub string_interner::symbol::SymbolU32);
|
||||
use derive_more::{Constructor, IsVariant, Unwrap};
|
||||
|
||||
/// 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 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.
|
||||
#[derive(Default, Clone, PartialEq)]
|
||||
#[derive(Constructor, Default, Clone, PartialEq)]
|
||||
pub struct AttrSet {
|
||||
data: BTreeMap<StaticSymbol, Value>,
|
||||
}
|
||||
|
||||
impl AttrSet {
|
||||
pub fn new(data: BTreeMap<StaticSymbol, Value>) -> Self {
|
||||
Self { data }
|
||||
}
|
||||
|
||||
/// Gets a value by key (string or Symbol).
|
||||
pub fn get<'a, 'sym: 'a>(&'a self, key: impl Into<Symbol<'sym>>) -> Option<&'a Value> {
|
||||
self.data.get(&key.into())
|
||||
@@ -336,17 +154,11 @@ impl Display for AttrSetCompatDisplay<'_> {
|
||||
}
|
||||
|
||||
/// Represents a Nix list, which is a vector of values.
|
||||
#[derive(Default, Clone, Debug, PartialEq)]
|
||||
#[derive(Constructor, Default, Clone, Debug, PartialEq)]
|
||||
pub struct List {
|
||||
data: Vec<Value>,
|
||||
}
|
||||
|
||||
impl List {
|
||||
pub fn new(data: Vec<Value>) -> Self {
|
||||
Self { data }
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for List {
|
||||
type Target = Vec<Value>;
|
||||
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.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[derive(IsVariant, Unwrap, Clone, Debug, PartialEq)]
|
||||
pub enum Value {
|
||||
/// An integer value.
|
||||
Int(i64),
|
||||
@@ -428,16 +240,35 @@ pub enum Value {
|
||||
/// A function (lambda).
|
||||
Func,
|
||||
/// A primitive (built-in) operation.
|
||||
PrimOp(&'static str),
|
||||
PrimOp(String),
|
||||
/// 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.
|
||||
/// This is used to prevent infinite recursion when printing or serializing cyclic data structures.
|
||||
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).
|
||||
pub struct NixFloat(pub f64);
|
||||
pub(crate) struct NixFloat(pub f64);
|
||||
|
||||
impl Display for NixFloat {
|
||||
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 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 decimal_places = (precision - 1 - exp) as usize;
|
||||
format!("{x:.decimal_places$}")
|
||||
@@ -489,9 +316,11 @@ fn fmt_nix_float(f: &mut Formatter<'_>, x: f64) -> FmtResult {
|
||||
};
|
||||
|
||||
if formatted.contains('.') {
|
||||
if let Some((head, tail)) = formatted.split_once('e') {
|
||||
let trimmed = head.trim_end_matches('0').trim_end_matches('.');
|
||||
write!(f, "{trimmed}e{tail}")
|
||||
if let Some(e_pos) = formatted.find('e') {
|
||||
let trimmed = formatted[..e_pos]
|
||||
.trim_end_matches('0')
|
||||
.trim_end_matches('.');
|
||||
write!(f, "{}{}", trimmed, &formatted[e_pos..])
|
||||
} else {
|
||||
let trimmed = formatted.trim_end_matches('0').trim_end_matches('.');
|
||||
write!(f, "{trimmed}")
|
||||
@@ -1,4 +1,4 @@
|
||||
use fix_lang::Value;
|
||||
use fix::value::Value;
|
||||
|
||||
use crate::utils::{eval_deep, eval_deep_result};
|
||||
|
||||
@@ -402,15 +402,17 @@ fn fixed_output_sha256_flat() {
|
||||
|
||||
#[test_log::test]
|
||||
fn fixed_output_missing_hashalgo() {
|
||||
eval_deep_result(
|
||||
r#"derivation {
|
||||
assert!(
|
||||
eval_deep_result(
|
||||
r#"derivation {
|
||||
name = "default";
|
||||
builder = "/bin/sh";
|
||||
system = "x86_64-linux";
|
||||
outputHash = "0000000000000000000000000000000000000000000000000000000000000000";
|
||||
}"#,
|
||||
)
|
||||
.unwrap_err();
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test_log::test]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use fix::Evaluator;
|
||||
use fix_error::Source;
|
||||
use fix_lang::Value;
|
||||
use fix::error::Source;
|
||||
use fix::runtime::Runtime;
|
||||
use fix::value::Value;
|
||||
|
||||
use crate::utils::{eval, eval_result};
|
||||
|
||||
@@ -97,7 +97,7 @@ fn import_with_complex_dependency_graph() {
|
||||
|
||||
#[test_log::test]
|
||||
fn path_with_file() {
|
||||
let mut ctx = Evaluator::new();
|
||||
let mut ctx = Runtime::new().unwrap();
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let test_file = temp_dir.path().join("test.txt");
|
||||
std::fs::write(&test_file, "Hello, World!").unwrap();
|
||||
@@ -136,7 +136,7 @@ fn path_with_custom_name() {
|
||||
|
||||
#[test_log::test]
|
||||
fn path_with_directory_recursive() {
|
||||
let mut ctx = Evaluator::new();
|
||||
let mut ctx = Runtime::new().unwrap();
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let test_dir = temp_dir.path().join("mydir");
|
||||
std::fs::create_dir_all(&test_dir).unwrap();
|
||||
@@ -159,7 +159,7 @@ fn path_with_directory_recursive() {
|
||||
|
||||
#[test_log::test]
|
||||
fn path_flat_with_file() {
|
||||
let mut ctx = Evaluator::new();
|
||||
let mut ctx = Runtime::new().unwrap();
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let test_file = temp_dir.path().join("flat.txt");
|
||||
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 result = eval_result(expr);
|
||||
|
||||
result.unwrap_err();
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test_log::test]
|
||||
|
||||
+231
-250
@@ -2,10 +2,9 @@
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use fix::Evaluator;
|
||||
use fix_error::{Source, SourceType};
|
||||
use fix_lang::Value;
|
||||
use serial_test::serial;
|
||||
use fix::error::Source;
|
||||
use fix::runtime::Runtime;
|
||||
use fix::value::Value;
|
||||
|
||||
fn get_lang_dir() -> PathBuf {
|
||||
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 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 source = Source {
|
||||
ty: SourceType::File(nix_path.into()),
|
||||
ty: fix::error::SourceType::File(nix_path.into()),
|
||||
src: content.into(),
|
||||
};
|
||||
ctx.eval_deep(source.clone())
|
||||
@@ -90,248 +89,230 @@ macro_rules! eval_fail_test {
|
||||
};
|
||||
}
|
||||
|
||||
mod okay {
|
||||
use super::*;
|
||||
eval_okay_test!(any_all);
|
||||
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_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!(
|
||||
#[serial(env)]
|
||||
getenv,
|
||||
|| {
|
||||
// SAFETY: guarded with #[serial_test::serial]
|
||||
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!(
|
||||
#[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);
|
||||
}
|
||||
eval_fail_test!(fail_abort);
|
||||
eval_fail_test!(fail_addDrvOutputDependencies_empty_context);
|
||||
eval_fail_test!(fail_addDrvOutputDependencies_multi_elem_context);
|
||||
eval_fail_test!(fail_addDrvOutputDependencies_wrong_element_kind);
|
||||
eval_fail_test!(fail_addErrorRuntime_example);
|
||||
eval_fail_test!(fail_assert);
|
||||
eval_fail_test!(fail_assert_equal_attrs_names);
|
||||
eval_fail_test!(fail_assert_equal_attrs_names_2);
|
||||
eval_fail_test!(fail_assert_equal_derivations);
|
||||
eval_fail_test!(fail_assert_equal_derivations_extra);
|
||||
eval_fail_test!(fail_assert_equal_floats);
|
||||
eval_fail_test!(fail_assert_equal_function_direct);
|
||||
eval_fail_test!(fail_assert_equal_int_float);
|
||||
eval_fail_test!(fail_assert_equal_ints);
|
||||
eval_fail_test!(fail_assert_equal_list_length);
|
||||
eval_fail_test!(fail_assert_equal_paths);
|
||||
eval_fail_test!(fail_assert_equal_type);
|
||||
eval_fail_test!(fail_assert_equal_type_nested);
|
||||
eval_fail_test!(fail_assert_nested_bool);
|
||||
eval_fail_test!(fail_attr_name_type);
|
||||
eval_fail_test!(fail_attrset_merge_drops_later_rec);
|
||||
eval_fail_test!(fail_bad_string_interpolation_1);
|
||||
eval_fail_test!(fail_bad_string_interpolation_2);
|
||||
eval_fail_test!(fail_bad_string_interpolation_3);
|
||||
eval_fail_test!(fail_bad_string_interpolation_4);
|
||||
eval_fail_test!(fail_blackhole);
|
||||
eval_fail_test!(fail_call_primop);
|
||||
eval_fail_test!(fail_deepseq);
|
||||
eval_fail_test!(fail_derivation_name);
|
||||
eval_fail_test!(fail_dup_dynamic_attrs);
|
||||
eval_fail_test!(fail_duplicate_traces);
|
||||
eval_fail_test!(fail_eol_1);
|
||||
eval_fail_test!(fail_eol_2);
|
||||
eval_fail_test!(fail_eol_3);
|
||||
eval_fail_test!(fail_fetchTree_negative);
|
||||
eval_fail_test!(fail_fetchurl_baseName);
|
||||
eval_fail_test!(fail_fetchurl_baseName_attrs);
|
||||
eval_fail_test!(fail_fetchurl_baseName_attrs_name);
|
||||
eval_fail_test!(fail_flake_ref_to_string_negative_integer);
|
||||
eval_fail_test!(fail_foldlStrict_strict_op_application);
|
||||
eval_fail_test!(fail_fromJSON_keyWithNullByte);
|
||||
eval_fail_test!(fail_fromJSON_overflowing);
|
||||
eval_fail_test!(fail_fromJSON_valueWithNullByte);
|
||||
eval_fail_test!(fail_fromTOML_keyWithNullByte);
|
||||
eval_fail_test!(fail_fromTOML_timestamps);
|
||||
eval_fail_test!(fail_fromTOML_valueWithNullByte);
|
||||
eval_fail_test!(fail_hashfile_missing);
|
||||
eval_fail_test!(fail_infinite_recursion_lambda);
|
||||
eval_fail_test!(fail_list);
|
||||
eval_fail_test!(fail_missing_arg);
|
||||
eval_fail_test!(fail_mutual_recursion);
|
||||
eval_fail_test!(fail_nested_list_items);
|
||||
eval_fail_test!(fail_nonexist_path);
|
||||
eval_fail_test!(fail_not_throws);
|
||||
eval_fail_test!(fail_overflowing_add);
|
||||
eval_fail_test!(fail_overflowing_div);
|
||||
eval_fail_test!(fail_overflowing_mul);
|
||||
eval_fail_test!(fail_overflowing_sub);
|
||||
eval_fail_test!(fail_path_slash);
|
||||
eval_fail_test!(fail_pipe_operators);
|
||||
eval_fail_test!(fail_recursion);
|
||||
eval_fail_test!(fail_remove);
|
||||
eval_fail_test!(fail_scope_5);
|
||||
eval_fail_test!(fail_seq);
|
||||
eval_fail_test!(fail_set);
|
||||
eval_fail_test!(fail_set_override);
|
||||
eval_fail_test!(fail_string_nul_1);
|
||||
eval_fail_test!(fail_string_nul_2);
|
||||
eval_fail_test!(fail_substring);
|
||||
eval_fail_test!(fail_toJSON);
|
||||
eval_fail_test!(fail_toJSON_non_utf_8);
|
||||
eval_fail_test!(fail_to_path);
|
||||
eval_fail_test!(fail_undeclared_arg);
|
||||
eval_fail_test!(fail_using_set_as_attr_name);
|
||||
|
||||
@@ -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 findfile;
|
||||
mod io_operations;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use fix::Evaluator;
|
||||
use fix_lang::Value;
|
||||
use fix::runtime::Runtime;
|
||||
use fix::value::Value;
|
||||
|
||||
use crate::utils::eval_result;
|
||||
|
||||
@@ -153,7 +153,7 @@ fn string_add_merges_context() {
|
||||
|
||||
#[test_log::test]
|
||||
fn context_in_derivation_args() {
|
||||
let mut rt = Evaluator::new();
|
||||
let mut rt = Runtime::new().unwrap();
|
||||
let result = rt
|
||||
.eval(
|
||||
r#"
|
||||
@@ -182,7 +182,7 @@ fn context_in_derivation_args() {
|
||||
|
||||
#[test_log::test]
|
||||
fn context_in_derivation_env() {
|
||||
let mut rt = Evaluator::new();
|
||||
let mut rt = Runtime::new().unwrap();
|
||||
let result = rt
|
||||
.eval(
|
||||
r#"
|
||||
@@ -224,7 +224,7 @@ fn tostring_preserves_context() {
|
||||
|
||||
#[test_log::test]
|
||||
fn interpolation_derivation_returns_outpath() {
|
||||
let mut rt = Evaluator::new();
|
||||
let mut rt = Runtime::new().unwrap();
|
||||
let result = rt
|
||||
.eval(
|
||||
r#"
|
||||
@@ -348,7 +348,7 @@ fn substring_zero_length_empty_value() {
|
||||
}
|
||||
|
||||
#[test_log::test]
|
||||
#[expect(non_snake_case)]
|
||||
#[allow(non_snake_case)]
|
||||
fn concatStringsSep_preserves_context() {
|
||||
let result = eval(
|
||||
r#"
|
||||
@@ -365,7 +365,7 @@ fn concatStringsSep_preserves_context() {
|
||||
}
|
||||
|
||||
#[test_log::test]
|
||||
#[expect(non_snake_case)]
|
||||
#[allow(non_snake_case)]
|
||||
fn concatStringsSep_merges_contexts() {
|
||||
let result = eval(
|
||||
r#"
|
||||
@@ -383,7 +383,7 @@ fn concatStringsSep_merges_contexts() {
|
||||
}
|
||||
|
||||
#[test_log::test]
|
||||
#[expect(non_snake_case)]
|
||||
#[allow(non_snake_case)]
|
||||
fn concatStringsSep_separator_has_context() {
|
||||
let result = eval(
|
||||
r#"
|
||||
@@ -398,7 +398,7 @@ fn concatStringsSep_separator_has_context() {
|
||||
}
|
||||
|
||||
#[test_log::test]
|
||||
#[expect(non_snake_case)]
|
||||
#[allow(non_snake_case)]
|
||||
fn replaceStrings_input_context_preserved() {
|
||||
let result = eval(
|
||||
r#"
|
||||
@@ -413,7 +413,7 @@ fn replaceStrings_input_context_preserved() {
|
||||
}
|
||||
|
||||
#[test_log::test]
|
||||
#[expect(non_snake_case)]
|
||||
#[allow(non_snake_case)]
|
||||
fn replaceStrings_replacement_context_collected() {
|
||||
let result = eval(
|
||||
r#"
|
||||
@@ -428,7 +428,7 @@ fn replaceStrings_replacement_context_collected() {
|
||||
}
|
||||
|
||||
#[test_log::test]
|
||||
#[expect(non_snake_case)]
|
||||
#[allow(non_snake_case)]
|
||||
fn replaceStrings_merges_contexts() {
|
||||
let result = eval(
|
||||
r#"
|
||||
@@ -446,7 +446,7 @@ fn replaceStrings_merges_contexts() {
|
||||
}
|
||||
|
||||
#[test_log::test]
|
||||
#[expect(non_snake_case)]
|
||||
#[allow(non_snake_case)]
|
||||
fn replaceStrings_lazy_evaluation_context() {
|
||||
let result = eval(
|
||||
r#"
|
||||
@@ -461,7 +461,7 @@ fn replaceStrings_lazy_evaluation_context() {
|
||||
}
|
||||
|
||||
#[test_log::test]
|
||||
#[expect(non_snake_case)]
|
||||
#[allow(non_snake_case)]
|
||||
fn baseNameOf_preserves_context() {
|
||||
let result = eval(
|
||||
r#"
|
||||
|
||||
@@ -1,29 +1,38 @@
|
||||
use fix::Evaluator;
|
||||
use fix_error::{Result, Source};
|
||||
use fix_lang::Value;
|
||||
#![allow(dead_code)]
|
||||
|
||||
use fix::error::{Result, Source};
|
||||
use fix::runtime::Runtime;
|
||||
use fix::value::Value;
|
||||
|
||||
pub fn eval(expr: &str) -> Value {
|
||||
Evaluator::new()
|
||||
Runtime::new()
|
||||
.unwrap()
|
||||
.eval(Source::new_eval(expr.into()).unwrap())
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn eval_shallow(expr: &str) -> Value {
|
||||
Evaluator::new()
|
||||
Runtime::new()
|
||||
.unwrap()
|
||||
.eval_shallow(Source::new_eval(expr.into()).unwrap())
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn eval_deep(expr: &str) -> Value {
|
||||
Evaluator::new()
|
||||
Runtime::new()
|
||||
.unwrap()
|
||||
.eval_deep(Source::new_eval(expr.into()).unwrap())
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
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> {
|
||||
Evaluator::new().eval(Source::new_eval(expr.into()).unwrap())
|
||||
Runtime::new()
|
||||
.unwrap()
|
||||
.eval(Source::new_eval(expr.into()).unwrap())
|
||||
}
|
||||
|
||||
Generated
+13
-13
@@ -8,11 +8,11 @@
|
||||
"rust-analyzer-src": "rust-analyzer-src"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1784017020,
|
||||
"narHash": "sha256-49WO85egjNtN1vMgJ3zUjDh5IqO+ou4zZY80WQ6EKGg=",
|
||||
"lastModified": 1774596377,
|
||||
"narHash": "sha256-DiSLMxyTwIUAlhOe34r6kKNQRv6PTF+vf0MG45mAyn4=",
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"rev": "fa2a0be0f712d7147d1677d33d15ea7b0589cdb4",
|
||||
"rev": "a88a1c8cf2f094da6347fcec54089f4bcb518409",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -24,11 +24,11 @@
|
||||
"flake-compat": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1777699697,
|
||||
"narHash": "sha256-Eg9b/rq/ECYwNwEXs5i9wHyhxNI0JrYx2srdI2uZMaQ=",
|
||||
"rev": "382052b74656a369c5408822af3f2501e9b1af81",
|
||||
"lastModified": 1751685974,
|
||||
"narHash": "sha256-NKw96t+BgHIYzHUjkTK95FqYRVKB8DHpVhefWSz/kTw=",
|
||||
"rev": "549f2762aebeff29a2e5ece7a7dc0f955281a1d1",
|
||||
"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": {
|
||||
"type": "tarball",
|
||||
@@ -37,11 +37,11 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1784007870,
|
||||
"narHash": "sha256-djcLt/JJphyNt4eDY9XTly+/WbCK5lqWq9lSgCmJkkQ=",
|
||||
"lastModified": 1774386573,
|
||||
"narHash": "sha256-4hAV26quOxdC6iyG7kYaZcM3VOskcPUrdCQd/nx8obc=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "18b9261cb3294b6d2a06d03f96872827b8fe2698",
|
||||
"rev": "46db2e09e1d3f113a13c0d7b81e2f221c63b8ce9",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -61,11 +61,11 @@
|
||||
"rust-analyzer-src": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1783975201,
|
||||
"narHash": "sha256-oyHWTfKWk06nTynCO8ByEAx/KEARvOOyTVnwinOmK3Q=",
|
||||
"lastModified": 1774569884,
|
||||
"narHash": "sha256-E8iWEPzg7OnE0XXXjo75CX7xFauqzJuGZ5wSO9KS8Ek=",
|
||||
"owner": "rust-lang",
|
||||
"repo": "rust-analyzer",
|
||||
"rev": "63a6f0d4bcfd3bbcf36383fcbcbcd93456ed1653",
|
||||
"rev": "443ddcddd0c73b07b799d052f5ef3b448c2f3508",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -8,23 +8,43 @@
|
||||
flake = false;
|
||||
};
|
||||
};
|
||||
outputs =
|
||||
{ nixpkgs, fenix, ... }:
|
||||
outputs = { nixpkgs, fenix, ... }:
|
||||
let
|
||||
forAllSystems = nixpkgs.lib.genAttrs nixpkgs.lib.systems.flakeExposed;
|
||||
in
|
||||
{
|
||||
devShells = forAllSystems (
|
||||
system:
|
||||
let
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
config.allowUnfree = true;
|
||||
overlays = [ fenix.overlays.default ];
|
||||
};
|
||||
in
|
||||
devShells = forAllSystems (system:
|
||||
let pkgs = import nixpkgs { inherit system; config.allowUnfree = true; }; 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
|
||||
];
|
||||
};
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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
@@ -1,7 +1,7 @@
|
||||
[files]
|
||||
extend-exclude = [
|
||||
"nix-js/tests/tests/regex.rs",
|
||||
"nix-js/tests/tests/lang",
|
||||
"nix-js/tests/tests/regex.rs",
|
||||
"nix-js/tests/tests/lang",
|
||||
]
|
||||
|
||||
[default.extend-words]
|
||||
|
||||
Reference in New Issue
Block a user