This commit is contained in:
2026-04-04 11:34:54 +08:00
parent ee54ab8895
commit b1b886229b
34 changed files with 1811 additions and 4222 deletions
Generated
+193 -2163
View File
File diff suppressed because it is too large Load Diff
+27
View File
@@ -2,8 +2,35 @@
resolver = "3"
members = [
"fix",
"fix-builtins",
"fix-codegen",
"fix-common",
"fix-error",
"fix-ir",
"fix-vm",
]
[profile.profiling]
inherits = "release"
debug = true
[workspace.dependencies]
bumpalo = { version = "3.20", features = [
"allocator-api2",
"boxed",
"collections",
] }
ghost-cell = "0.2"
hashbrown = "0.16"
num_enum = "0.7.5"
smallvec = "1.15"
ere = "0.2"
string-interner = "0.19"
rnix = "0.14"
rowan = "0.16"
likely_stable = "0.1"
[workspace.dependencies.gc-arena]
git = "https://github.com/kyren/gc-arena"
rev = "75671ae03f53718357b741ed4027560f14e90836"
features = ["allocator-api2", "hashbrown", "smallvec"]
+8
View File
@@ -0,0 +1,8 @@
[package]
name = "fix-builtins"
version = "0.1.0"
edition = "2024"
[dependencies]
num_enum = { workspace = true }
gc-arena = { workspace = true }
+125
View File
@@ -0,0 +1,125 @@
use gc_arena::Collect;
use num_enum::TryFromPrimitive;
macro_rules! define_builtins {
($(($name:literal, $variant:ident, $arity:expr)),* $(,)?) => {
/// Builtin function registry.
/// Array index IS the PrimOp id. (name, arity) pairs.
pub const BUILTINS: &[(&str, u8)] = &[
$(($name, $arity),)*
];
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, TryFromPrimitive, Collect)]
#[repr(u8)]
#[collect(require_static)]
pub enum BuiltinId {
$($variant,)*
}
};
}
define_builtins! {
("abort", Abort, 1),
("__add", Add, 2),
("__addErrorContext", AddErrorContext, 2),
("__all", All, 2),
("__any", Any, 2),
("__appendContext", AppendContext, 2),
("__attrNames", AttrNames, 1),
("__attrValues", AttrValues, 1),
("baseNameOf", BaseNameOf, 1),
("__bitAnd", BitAnd, 2),
("__bitOr", BitOr, 2),
("__bitXor", BitXor, 2),
("break", Break, 1),
("__catAttrs", CatAttrs, 2),
("__ceil", Ceil, 1),
("__compareVersions", CompareVersions, 2),
("__concatLists", ConcatLists, 1),
("__concatMap", ConcatMap, 2),
("__concatStringsSep", ConcatStringsSep, 2),
("__convertHash", ConvertHash, 1),
("__deepSeq", DeepSeq, 2),
("derivation", Derivation, 1),
("derivationStrict", DerivationStrict, 1),
("dirOf", DirOf, 1),
("__div", Div, 2),
("__elem", Elem, 2),
("__elemAt", ElemAt, 2),
("fetchGit", FetchGit, 1),
("fetchMercurial", FetchMercurial, 1),
("fetchTarball", FetchTarball, 1),
("fetchTree", FetchTree, 1),
("__fetchurl", FetchUrl, 1),
("__filter", Filter, 2),
("__filterSource", FilterSource, 2),
("__findFile", FindFile, 2),
("__floor", Floor, 1),
("__foldl'", FoldlStrict, 3),
("__fromJSON", FromJSON, 1),
("fromTOML", FromTOML, 1),
("__functionArgs", FunctionArgs, 1),
("__genList", GenList, 2),
("__genericClosure", GenericClosure, 1),
("__getAttr", GetAttr, 2),
("__getContext", GetContext, 1),
("__getEnv", GetEnv, 1),
("__groupBy", GroupBy, 2),
("__hasAttr", HasAttr, 2),
("__hasContext", HasContext, 1),
("__hashFile", HashFile, 2),
("__hashString", HashString, 2),
("__head", Head, 1),
("import", Import, 1),
("__intersectAttrs", IntersectAttrs, 2),
("__isAttrs", IsAttrs, 1),
("__isBool", IsBool, 1),
("__isFloat", IsFloat, 1),
("__isFunction", IsFunction, 1),
("__isInt", IsInt, 1),
("__isList", IsList, 1),
("isNull", IsNull, 1),
("__isPath", IsPath, 1),
("__isString", IsString, 1),
("__length", Length, 1),
("__lessThan", LessThan, 2),
("__listToAttrs", ListToAttrs, 1),
("map", Map, 2),
("__mapAttrs", MapAttrs, 2),
("__match", Match, 2),
("__mul", Mul, 2),
("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),
}
+15
View File
@@ -0,0 +1,15 @@
[package]
name = "fix-codegen"
version = "0.1.0"
edition = "2024"
[dependencies]
gc-arena = { workspace = true }
hashbrown = { workspace = true }
num_enum = { workspace = true }
rnix = { workspace = true }
string-interner = { workspace = true }
fix-builtins = { path = "../fix-builtins" }
fix-common = { path = "../fix-common" }
fix-ir = { path = "../fix-ir" }
+54 -47
View File
@@ -1,30 +1,21 @@
use std::ops::Deref;
use gc_arena::Collect;
use fix_builtins::{BUILTINS, BuiltinId};
use fix_common::StringId;
use fix_ir::{Attr, BinOpKind, Ir, Param, RawIrRef, ThunkId, UnOpKind};
use hashbrown::HashMap;
use num_enum::TryFromPrimitive;
use rnix::TextRange;
use string_interner::Symbol as _;
use crate::ir::{Attr, BinOpKind, Ir, Param, RawIrRef, StringId, ThunkId, UnOpKind};
use crate::runtime::BUILTINS;
use crate::runtime::value::{Null, PrimOp, StaticValue};
pub struct InstructionPtr(pub usize);
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 {
pub 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: crate::runtime::value::StaticValue) -> u32;
fn add_constant(&mut self, val: Const) -> u32;
}
#[repr(u8)]
@@ -49,7 +40,6 @@ pub enum Op {
MakePatternClosure,
Call,
CallNoSpan,
MakeAttrs,
MakeEmptyAttrs,
@@ -98,6 +88,8 @@ pub enum Op {
LoadScopedBinding,
Return,
Illegal,
}
struct ScopeInfo {
@@ -110,22 +102,39 @@ struct BytecodeEmitter<'a, Ctx: BytecodeContext> {
scope_stack: Vec<ScopeInfo>,
}
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;
#[repr(u8)]
#[derive(Debug, Clone, Copy, TryFromPrimitive)]
pub enum OperandType {
Const,
Local,
Builtins,
BigInt,
}
pub(crate) const KEY_STATIC: u8 = 0;
pub(crate) const KEY_DYNAMIC: u8 = 1;
#[repr(u8)]
#[derive(Debug, Clone, Copy, TryFromPrimitive)]
pub enum AttrKeyType {
Static,
Dynamic,
}
enum InlineOperand {
Const(StaticValue),
pub enum Const {
Smi(i32),
Float(f64),
Bool(bool),
String(StringId),
PrimOp { id: BuiltinId, arity: u8 },
Null,
}
pub enum InlineOperand {
Const(Const),
Local { layer: u16, local: u32 },
Builtins,
BigInt(i64),
}
pub(crate) fn compile_bytecode(ir: RawIrRef<'_>, ctx: &mut impl BytecodeContext) -> InstructionPtr {
pub 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);
@@ -144,17 +153,17 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
match ir.deref() {
&Ir::Int(x) => {
if x <= i32::MAX as i64 {
InlineOperand::Const(StaticValue::new_inline(x as i32))
InlineOperand::Const(Const::Smi(x as i32))
} else {
InlineOperand::BigInt(x)
}
}
&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::Float(x) => InlineOperand::Const(Const::Float(x)),
&Ir::Bool(b) => InlineOperand::Const(Const::Bool(b)),
Ir::Null => InlineOperand::Const(Const::Null),
Ir::Str(s) => {
let sid = self.ctx.intern_string(s.deref());
InlineOperand::Const(StaticValue::new_inline(sid))
InlineOperand::Const(Const::String(sid))
}
&Ir::Thunk(id) => {
let (layer, local) = self.resolve_thunk(id);
@@ -166,7 +175,7 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
},
&Ir::Builtin(id) => {
let arity = BUILTINS[id as usize].1;
InlineOperand::Const(StaticValue::new_inline(PrimOp { id, arity }))
InlineOperand::Const(Const::PrimOp { id, arity })
}
Ir::Builtins => InlineOperand::Builtins,
_ => panic!("cannot classify IR node as inline operand"),
@@ -177,19 +186,19 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
match operand {
InlineOperand::Const(val) => {
let idx = self.ctx.add_constant(val);
self.emit_u8(OPERAND_CONST);
self.emit_u8(OperandType::Const as u8);
self.emit_u32(idx);
}
InlineOperand::Local { layer, local } => {
self.emit_u8(OPERAND_LOCAL);
self.emit_u8(OperandType::Local as u8);
self.emit_u8(layer as u8);
self.emit_u32(local);
}
InlineOperand::Builtins => {
self.emit_u8(OPERAND_BUILTINS);
self.emit_u8(OperandType::Builtins as u8);
}
InlineOperand::BigInt(val) => {
self.emit_u8(OPERAND_BIGINT);
self.emit_u8(OperandType::BigInt as u8);
self.emit_i64(val);
}
}
@@ -566,12 +575,10 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
}
}
}
&Ir::Call { func, arg, span } => {
&Ir::Call { func, arg, .. } => {
self.emit_expr(arg);
self.emit_expr(func);
let span_id = self.ctx.register_span(span);
self.emit_op(Op::Call);
self.emit_u32(span_id);
}
&Ir::Arg { layer } => {
self.emit_load(layer.try_into().expect("scope too deep!"), 0);
@@ -725,12 +732,12 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
PipeL => {
self.emit_expr(rhs);
self.emit_expr(lhs);
self.emit_op(Op::CallNoSpan);
self.emit_op(Op::Call);
}
PipeR => {
self.emit_expr(lhs);
self.emit_expr(rhs);
self.emit_op(Op::CallNoSpan);
self.emit_op(Op::Call);
}
_ => {
self.emit_expr(lhs);
@@ -808,7 +815,7 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
fn emit_attrset(
&mut self,
stcs: &crate::ir::HashMap<'_, StringId, (RawIrRef<'_>, TextRange)>,
stcs: &fix_ir::HashMap<'_, StringId, (RawIrRef<'_>, TextRange)>,
dyns: &[(RawIrRef<'_>, RawIrRef<'_>, TextRange)],
) {
if stcs.is_empty() && dyns.is_empty() {
@@ -821,7 +828,7 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
self.emit_u32(total as u32);
for (&sym, &(val, span)) in stcs.iter() {
self.emit_u8(KEY_STATIC);
self.emit_u8(AttrKeyType::Static as u8);
self.emit_str_id(sym);
let val_operand = self.classify_value(val);
self.emit_inline_operand(val_operand);
@@ -829,7 +836,7 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
self.emit_u32(span_id);
}
for &(key, val, span) in dyns.iter() {
self.emit_u8(KEY_DYNAMIC);
self.emit_u8(AttrKeyType::Dynamic as u8);
let key_operand = self.classify_value(key);
self.emit_inline_operand(key_operand);
let val_operand = self.classify_value(val);
@@ -867,11 +874,11 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
for attr in attrpath.iter() {
match *attr {
Attr::Str(sym, _) => {
self.emit_u8(KEY_STATIC);
self.emit_u8(AttrKeyType::Static as u8);
self.emit_str_id(sym);
}
Attr::Dynamic(_, _) => {
self.emit_u8(KEY_DYNAMIC);
self.emit_u8(AttrKeyType::Dynamic as u8);
}
}
}
@@ -889,11 +896,11 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
for attr in rhs.iter() {
match *attr {
Attr::Str(sym, _) => {
self.emit_u8(KEY_STATIC);
self.emit_u8(AttrKeyType::Static as u8);
self.emit_str_id(sym);
}
Attr::Dynamic(_, _) => {
self.emit_u8(KEY_DYNAMIC);
self.emit_u8(AttrKeyType::Dynamic as u8);
}
}
}
+9
View File
@@ -0,0 +1,9 @@
[package]
name = "fix-common"
version = "0.1.0"
edition = "2024"
[dependencies]
gc-arena = { workspace = true }
string-interner = { workspace = true }
ere = { workspace = true }
+42 -29
View File
@@ -1,14 +1,17 @@
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::ops::DerefMut;
use std::fmt::{Debug, Display, Formatter, Result as FmtResult};
use std::ops::{Deref, DerefMut};
use derive_more::{Constructor, IsVariant, Unwrap};
use gc_arena::Collect;
#[repr(transparent)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Collect)]
#[collect(require_static)]
pub struct StringId(pub string_interner::symbol::SymbolU32);
/// Represents a Nix symbol, which is used as a key in attribute sets.
#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Constructor)]
#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub struct Symbol<'a>(Cow<'a, str>);
pub type StaticSymbol = Symbol<'static>;
@@ -60,13 +63,36 @@ 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(Constructor, Default, Clone, PartialEq)]
#[derive(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())
@@ -154,11 +180,17 @@ impl Display for AttrSetCompatDisplay<'_> {
}
/// Represents a Nix list, which is a vector of values.
#[derive(Constructor, Default, Clone, Debug, PartialEq)]
#[derive(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 {
@@ -217,7 +249,7 @@ impl Display for ListCompatDisplay<'_> {
}
/// Represents any possible Nix value that can be returned from an evaluation.
#[derive(IsVariant, Unwrap, Clone, Debug, PartialEq)]
#[derive(Clone, Debug, PartialEq)]
pub enum Value {
/// An integer value.
Int(i64),
@@ -248,27 +280,8 @@ pub enum Value {
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(crate) struct NixFloat(pub f64);
pub struct NixFloat(pub f64);
impl Display for NixFloat {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
+9
View File
@@ -0,0 +1,9 @@
[package]
name = "fix-error"
version = "0.1.0"
edition = "2024"
[dependencies]
miette = { version = "7.6", features = ["fancy"] }
thiserror = "2.0"
rnix = { workspace = true }
+223
View File
@@ -0,0 +1,223 @@
use std::path::{Path, PathBuf};
use std::sync::Arc;
use miette::{Diagnostic, NamedSource, SourceSpan};
use thiserror::Error;
pub type Result<T> = core::result::Result<T, Box<Error>>;
#[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>>,
}
+17
View File
@@ -0,0 +1,17 @@
[package]
name = "fix-ir"
version = "0.1.0"
edition = "2024"
[dependencies]
bumpalo = { workspace = true }
ghost-cell = { workspace = true }
rnix = { workspace = true }
rowan = { workspace = true }
string-interner = { workspace = true }
hashbrown = { workspace = true }
num_enum = { workspace = true }
fix-builtins = { path = "../fix-builtins" }
fix-common = { path = "../fix-common" }
fix-error = { path = "../fix-error" }
@@ -1,15 +1,15 @@
use bumpalo::boxed::Box;
use bumpalo::collections::{CollectIn, Vec};
use fix_builtins::BuiltinId;
use fix_common::Symbol;
use fix_error::{Error, Result, Source};
use hashbrown::HashSet;
use hashbrown::hash_map::Entry;
use rnix::TextRange;
use rnix::ast::{self, Expr, HasEntry};
use rowan::ast::AstNode;
use crate::error::{Error, Result, Source};
use crate::ir::*;
use crate::runtime::BuiltinId;
use crate::value::Symbol;
use super::*;
trait Require<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>, T> {
fn require(self, ctx: &Ctx, span: TextRange) -> Result<T>;
@@ -151,8 +151,7 @@ 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<IrRef<'id, 'ir>> {
let span = self.syntax().text_range();
downgrade_path(self.parts(), span, ctx)
downgrade_path(self.parts(), ctx)
}
}
};
@@ -1215,7 +1214,6 @@ 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<IrRef<'id, 'ir>> {
let bump = ctx.bump();
+49 -8
View File
@@ -4,12 +4,14 @@ use std::ops::Deref;
use bumpalo::Bump;
use bumpalo::boxed::Box;
use bumpalo::collections::Vec;
use gc_arena::Collect;
use fix_builtins::{BUILTINS, BuiltinId};
use fix_common::StringId;
use ghost_cell::{GhostCell, GhostToken};
use num_enum::TryFromPrimitive as _;
use rnix::{TextRange, ast};
use string_interner::symbol::SymbolU32;
use string_interner::DefaultStringInterner;
use crate::runtime::BuiltinId;
pub mod downgrade;
pub type HashMap<'ir, K, V> = hashbrown::HashMap<K, V, hashbrown::DefaultHashBuilder, &'ir Bump>;
@@ -157,11 +159,6 @@ pub enum Ir<'ir, Ref> {
#[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 SpanId(pub u32);
@@ -258,3 +255,47 @@ pub struct Param<'ir> {
pub optional: Vec<'ir, (StringId, TextRange)>,
pub ellipsis: bool,
}
pub fn new_global_env(
strings: &mut DefaultStringInterner,
) -> hashbrown::HashMap<StringId, Ir<'static, RawIrRef<'static>>> {
let mut global_env = hashbrown::HashMap::new();
let builtins_sym = StringId(strings.get_or_intern("builtins"));
global_env.insert(builtins_sym, Ir::Builtins);
for (idx, &(name, _)) in BUILTINS.iter().enumerate() {
let id = BuiltinId::try_from_primitive(idx as u8).expect("infallible");
let name = StringId(strings.get_or_intern(name));
global_env.insert(name, Ir::Builtin(id));
}
let consts = [
(
"__currentSystem",
Ir::BuiltinConst(StringId(strings.get_or_intern("currentSystem"))),
),
("__langVersion", Ir::Int(6)),
(
"__nixVersion",
Ir::BuiltinConst(StringId(strings.get_or_intern("nixVersion"))),
),
(
"__storeDir",
Ir::BuiltinConst(StringId(strings.get_or_intern("storeDir"))),
),
(
"__nixPath",
Ir::BuiltinConst(StringId(strings.get_or_intern("nixPath"))),
),
("null", Ir::Null),
("true", Ir::Bool(true)),
("false", Ir::Bool(false)),
];
for (name, ir) in consts {
let name = StringId(strings.get_or_intern(name));
global_env.insert(name, ir);
}
global_env
}
+18
View File
@@ -0,0 +1,18 @@
[package]
name = "fix-vm"
version = "0.1.0"
edition = "2024"
[dependencies]
gc-arena = { workspace = true }
hashbrown = { workspace = true }
num_enum = { workspace = true }
smallvec = { workspace = true }
string-interner = { workspace = true }
likely_stable = { workspace = true }
sptr = "0.3"
fix-builtins = { path = "../fix-builtins" }
fix-codegen = { path = "../fix-codegen" }
fix-common = { path = "../fix-common" }
fix-error = { path = "../fix-error" }
+3 -1
View File
@@ -1,3 +1,5 @@
#![allow(dead_code)]
use std::fmt;
use std::num::NonZeroU8;
@@ -22,7 +24,7 @@ impl<T: Default + Copy, const N: usize> ArrayExt<N> for [T; N] {
}
}
pub(crate) trait RawStore: Sized {
pub trait RawStore: Sized {
fn to_val(self, value: &mut Value);
fn from_val(value: &Value) -> Self;
}
+18
View File
@@ -0,0 +1,18 @@
use fix_error::Error;
use crate::value::StrictValue;
use crate::{NixNum, VmError};
pub(super) fn vm_err(msg: impl Into<String>) -> VmError {
VmError::Uncatchable(Error::eval_error(msg.into()))
}
pub(super) fn get_num(val: StrictValue<'_>) -> Option<NixNum> {
if let Some(i) = val.as_inline::<i32>() {
Some(NixNum::Int(i as i64))
} else if let Some(gc_i) = val.as_gc::<i64>() {
Some(NixNum::Int(*gc_i))
} else {
val.as_float().map(NixNum::Float)
}
}
File diff suppressed because it is too large Load Diff
@@ -1,7 +1,6 @@
use std::mem::MaybeUninit;
use gc_arena::Collect;
use smallvec::SmallVec;
pub(super) struct Stack<const N: usize, T> {
inner: Box<[MaybeUninit<T>; N]>,
@@ -74,15 +73,4 @@ impl<const N: usize, T> Stack<N, T> {
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
}
}
@@ -1,25 +1,30 @@
#![allow(dead_code)]
use std::fmt;
use std::marker::PhantomData;
use std::mem::size_of;
use std::ops::Deref;
use fix_builtins::BuiltinId;
use fix_common::*;
use gc_arena::collect::Trace;
use gc_arena::{Collect, Gc, Mutation, RefLock};
use gc_arena::{Collect, Gc, GcRefLock, Mutation, RefLock};
use num_enum::TryFromPrimitive;
use sealed::sealed;
use smallvec::SmallVec;
use string_interner::Symbol;
use string_interner::symbol::SymbolU32;
use crate::NixNum;
use crate::boxing::{RawBox, RawStore, RawTag, Value as RawValue};
use crate::ir::StringId;
use crate::runtime::builtins::BuiltinId;
#[sealed]
mod private {
pub trait Cealed {}
}
/// # Safety
/// 1. TAG must be unique among all implementors.
/// 2. TAG must be within 1..=7
pub(crate) unsafe trait Storable {
pub(crate) unsafe trait Storable: private::Cealed {
const TAG: (bool, u8);
}
pub(crate) trait InlineStorable: Storable + RawStore {}
@@ -31,18 +36,18 @@ macro_rules! define_value_types {
gc { $($gtype:ty => $gtag:expr, $gname:literal;)* }
) => {
$(
#[sealed]
unsafe impl Storable for $itype {
const TAG: (bool, u8) = $itag;
}
impl InlineStorable for $itype {}
impl private::Cealed for $itype {}
)*
$(
#[sealed]
unsafe impl Storable for $gtype {
const TAG: (bool, u8) = $gtag;
}
impl GcStorable for $gtype {}
impl private::Cealed for $gtype {}
)*
const _: () = assert!(size_of::<Value<'static>>() == 8);
@@ -98,20 +103,6 @@ macro_rules! define_value_types {
}
}
}
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})"),
}
}
}
};
}
@@ -166,7 +157,7 @@ impl<'gc> Value<'gc> {
///
/// 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 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);
@@ -176,7 +167,7 @@ impl<'gc> Value<'gc> {
/// Returns the `(negative, val)` tag, or `None` for a float.
#[inline(always)]
fn tag(&self) -> Option<(bool, u8)> {
fn tag(self) -> Option<(bool, u8)> {
self.raw.tag().map(|t| t.neg_val())
}
}
@@ -219,24 +210,24 @@ impl<'gc> Value<'gc> {
impl<'gc> Value<'gc> {
#[inline]
pub(crate) fn is_float(&self) -> bool {
pub(crate) fn is_float(self) -> bool {
self.raw.is_float()
}
#[inline]
pub(crate) fn is<T: Storable>(&self) -> bool {
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> {
pub(crate) fn as_float(self) -> Option<f64> {
self.raw.float().copied()
}
#[inline]
pub(crate) fn as_inline<T: InlineStorable>(&self) -> Option<T> {
pub(crate) fn as_inline<T: InlineStorable>(self) -> Option<T> {
if self.is::<T>() {
Some(unsafe {
let rv = self.raw.value().unwrap_unchecked();
@@ -248,7 +239,7 @@ impl<'gc> Value<'gc> {
}
#[inline]
pub(crate) fn as_gc<T: GcStorable>(&self) -> Option<Gc<'gc, T>> {
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();
@@ -260,6 +251,17 @@ impl<'gc> Value<'gc> {
}
}
#[inline]
pub(crate) fn as_num(self) -> Option<NixNum> {
if let Some(i) = self.as_inline::<i32>() {
Some(NixNum::Int(i as i64))
} else if let Some(gc_i) = self.as_gc::<i64>() {
Some(NixNum::Int(*gc_i))
} else {
self.as_float().map(NixNum::Float)
}
}
#[inline]
pub(crate) fn restrict(self) -> Option<StrictValue<'gc>> {
if !self.is::<Thunk<'gc>>() {
@@ -270,88 +272,50 @@ impl<'gc> Value<'gc> {
}
}
#[derive(Copy, Clone)]
#[derive(Copy, Clone, Default)]
#[repr(transparent)]
pub(crate) struct StaticValue {
raw: RawBox,
}
pub struct StaticValue(Value<'static>);
impl Default for StaticValue {
#[inline(always)]
fn default() -> Self {
Self::new_inline(Null)
}
}
impl From<StaticValue> for Value<'_> {
impl<'gc> From<StaticValue> for Value<'gc> {
#[inline]
fn from(value: StaticValue) -> Self {
Self {
raw: value.raw,
_marker: PhantomData,
}
// SAFETY: StaticValue is guaranteed to not contain any `Gc`.
unsafe { std::mem::transmute::<Value<'static>, Value<'gc>>(value.0) }
}
}
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()
pub fn new_float(val: f64) -> Self {
Self(Value::new_float(val))
}
#[inline]
pub(crate) fn new_float(val: f64) -> Self {
Self {
raw: RawBox::from_float(val),
pub fn new_inline<T: InlineStorable>(val: T) -> Self {
Self(Value::new_inline(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,
))
pub fn new_primop(id: BuiltinId, arity: u8) -> Self {
Self(Value::new_inline(PrimOp { id, arity }))
}
#[inline]
pub(crate) fn is_float(&self) -> bool {
self.raw.is_float()
pub fn is_float(self) -> bool {
self.0.is_float()
}
#[inline]
pub(crate) fn is<T: InlineStorable>(&self) -> bool {
self.tag() == Some(T::TAG)
pub fn is<T: InlineStorable>(self) -> bool {
self.0.is::<T>()
}
#[inline]
pub(crate) fn as_float(&self) -> Option<f64> {
self.raw.float().copied()
pub fn as_float(self) -> Option<f64> {
self.0.as_float()
}
#[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
pub fn as_inline<T: InlineStorable>(self) -> Option<T> {
self.0.as_inline::<T>()
}
}
/// 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())
#[inline]
pub fn to_bits(self) -> u64 {
self.0.raw.to_bits()
}
}
@@ -420,7 +384,7 @@ impl<'gc> Deref for AttrSet<'gc> {
}
impl<'gc> AttrSet<'gc> {
pub(crate) unsafe fn from_sorted_unchecked(
pub(crate) fn from_sorted_unchecked(
entries: SmallVec<[(StringId, Value<'gc>); 4]>,
) -> Self {
debug_assert!(entries.is_sorted_by_key(|(key, _)| *key));
@@ -492,7 +456,8 @@ pub(crate) type Thunk<'gc> = RefLock<ThunkState<'gc>>;
pub(crate) enum ThunkState<'gc> {
Pending {
ip: usize,
env: Gc<'gc, RefLock<Env<'gc>>>,
env: GcEnv<'gc>,
with_env: Option<GcWithEnv<'gc>>,
},
Apply {
func: Value<'gc>,
@@ -506,8 +471,17 @@ pub(crate) enum ThunkState<'gc> {
#[collect(no_drop)]
pub(crate) struct Env<'gc> {
pub(crate) locals: SmallVec<[Value<'gc>; 4]>,
pub(crate) prev: Option<Gc<'gc, RefLock<Env<'gc>>>>,
pub(crate) prev: Option<GcEnv<'gc>>,
}
pub(crate) type GcEnv<'gc> = GcRefLock<'gc, Env<'gc>>;
#[derive(Collect, Debug)]
#[collect(no_drop)]
pub(crate) struct WithEnv<'gc> {
pub(crate) env: Value<'gc>,
pub(crate) prev: Option<GcWithEnv<'gc>>,
}
pub(crate) type GcWithEnv<'gc> = Gc<'gc, WithEnv<'gc>>;
impl<'gc> Env<'gc> {
pub(crate) fn empty() -> Self {
+17 -63
View File
@@ -6,20 +6,9 @@ edition = "2024"
[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 = "17.0"
rustyline = "18.0"
# CLI
clap = { version = "4", features = ["derive"] }
@@ -28,67 +17,32 @@ clap = { version = "4", features = ["derive"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
derive_more = { version = "2", features = ["full"] }
# Error Reporting
thiserror = "2"
miette = { version = "7.4", features = ["fancy"] }
hashbrown = "0.16"
string-interner = "0.19"
bumpalo = { version = "3.20", features = [
"allocator-api2",
"boxed",
"collections",
] }
# Data Structure
hashbrown = { workspace = true }
smallvec = { workspace = true }
string-interner = { workspace = true }
rust-embed = "8.11"
# Memory Management
bumpalo = { workspace = true }
regex = "1.11"
rnix = { workspace = true }
nix-nar = "0.3"
sha2 = "0.10"
sha1 = "0.10"
md5 = "0.8"
hex = "0.4"
ere = { workspace = true }
ghost-cell = { workspace = true }
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"]
fix-common = { path = "../fix-common" }
fix-codegen = { path = "../fix-codegen" }
fix-error = { path = "../fix-error" }
fix-ir = { path = "../fix-ir" }
fix-vm = { path = "../fix-vm" }
[dev-dependencies]
criterion = { version = "0.8", features = ["html_reports"] }
tempfile = "3.24"
test-log = { version = "0.2", features = ["trace"] }
[[bench]]
+5 -8
View File
@@ -1,18 +1,15 @@
#![allow(dead_code)]
use fix::error::{Result, Source};
use fix::runtime::Runtime;
use fix::value::Value;
use fix::Evaluator;
use fix_common::Value;
use fix_error::{Result, Source};
pub fn eval(expr: &str) -> Value {
Runtime::new()
.unwrap()
Evaluator::new()
.eval(Source::new_eval(expr.into()).unwrap())
.unwrap()
}
pub fn eval_result(expr: &str) -> Result<Value> {
Runtime::new()
.unwrap()
.eval(Source::new_eval(expr.into()).unwrap())
Evaluator::new().eval(Source::new_eval(expr.into()).unwrap())
}
-369
View File
@@ -1,369 +0,0 @@
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();
("MakeThunk", format!("-> {:04x}", offset))
}
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()),
}
}
}
+547 -14
View File
@@ -1,22 +1,555 @@
#![warn(clippy::unwrap_used)]
#![allow(dead_code)]
mod boxing;
pub mod error;
pub mod logging;
pub mod runtime;
pub mod value;
use bumpalo::Bump;
use fix_codegen::{BytecodeContext, InstructionPtr};
use fix_common::{StringId, Symbol};
use fix_error::{Error, Result, Source};
use fix_ir::downgrade::{Downgrade as _, DowngradeContext};
use fix_ir::{Ir, IrRef, RawIrRef, ThunkId};
use fix_vm::{ForceMode, StaticValue, Vm, VmContext};
use ghost_cell::{GhostCell, GhostToken};
use hashbrown::{HashMap, HashSet};
use string_interner::DefaultStringInterner;
mod codegen;
mod derivation;
mod disassembler;
mod downgrade;
// mod fetcher;
mod ir;
mod nar;
mod nix_utils;
mod store;
mod string_context;
// mod nar;
// mod nix_utils;
// mod store;
// mod string_context;
mod derivation;
pub mod logging;
#[global_allocator]
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
pub struct Evaluator {
bytecode: Vec<u8>,
constants: Constants,
strings: DefaultStringInterner,
sources: Vec<Source>,
spans: Vec<(usize, rnix::TextRange)>,
// FIXME: remove?
thunk_count: usize,
global_env: HashMap<StringId, Ir<'static, RawIrRef<'static>>>,
}
impl Default for Evaluator {
fn default() -> Self {
Self::new()
}
}
impl Evaluator {
pub fn new() -> Self {
let mut strings = DefaultStringInterner::new();
let global_env = fix_ir::new_global_env(&mut strings);
Self {
sources: Vec::new(),
spans: Vec::new(),
strings,
thunk_count: 0,
bytecode: Vec::new(),
constants: Constants::default(),
global_env,
}
}
pub fn eval(&mut self, source: Source) -> Result<fix_common::Value> {
self.do_eval(source, None, ForceMode::AsIs)
}
pub fn eval_shallow(&mut self, source: Source) -> Result<fix_common::Value> {
self.do_eval(source, None, ForceMode::Shallow)
}
pub fn eval_deep(&mut self, source: Source) -> Result<fix_common::Value> {
self.do_eval(source, None, ForceMode::Deep)
}
pub fn eval_repl(
&mut self,
source: Source,
scope: &HashSet<StringId>,
) -> Result<fix_common::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<fix_common::Value> {
let root = self.downgrade(source, extra_scope)?;
let ip = fix_codegen::compile_bytecode(root.as_ref(), self);
let vm = Vm::new(self, ip, force_mode);
vm.run()
}
pub fn add_binding(
&mut self,
_ident: &str,
_expr: &str,
_scope: &mut HashSet<StringId>,
) -> Result<fix_common::Value> {
todo!()
}
pub fn compile_bytecode(&mut self, source: Source) -> Result<InstructionPtr> {
let root = self.downgrade(source, None)?;
let ip = fix_codegen::compile_bytecode(root.as_ref(), self);
Ok(ip)
}
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 Self {
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 })
})
}
}
impl VmContext for &mut Evaluator {
fn intern_string(&mut self, s: impl AsRef<str>) -> StringId {
StringId(self.strings.get_or_intern(s))
}
fn resolve_string(&self, id: StringId) -> &str {
#[allow(clippy::unwrap_used)]
self.strings.resolve(id.0).unwrap()
}
fn bytecode(&self) -> &[u8] {
&self.bytecode
}
fn get_const(&self, id: u32) -> StaticValue {
#[allow(clippy::unwrap_used)]
self.constants.get(id).unwrap()
}
fn compile(&mut self, _source: Source) {
todo!();
}
}
#[derive(Default)]
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()
}
}
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 maybe_thunk(&mut self, ir: IrRef<'id, 'ir>) -> IrRef<'id, 'ir> {
if !should_thunk(ir, &self.token) {
return ir;
}
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: rnix::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 {
sym: param_sym,
abs_layer,
} => {
if param_sym == sym {
return Ok(self.new_expr(Ir::Arg {
layer: self.thunk_scopes.len() - abs_layer,
}));
}
}
}
}
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(base + offset)));
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, sym: StringId, f: F) -> R
where
F: FnOnce(&mut Self) -> R,
{
self.scopes.push(Scope::Param {
sym,
abs_layer: self.thunk_scopes.len(),
});
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>)>,
}
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: IrRef<'id, 'ir>, _token: &GhostToken<'id>) {
self.bindings.push((id, ir));
}
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 { sym: StringId, abs_layer: usize },
}
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 Evaluator {
fn intern_string(&mut self, s: &str) -> StringId {
StringId(self.strings.get_or_intern(s))
}
fn register_span(&mut self, range: rnix::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: fix_codegen::Const) -> u32 {
use fix_codegen::Const::*;
let val = match val {
Smi(x) => StaticValue::new_inline(x),
Float(x) => StaticValue::new_float(x),
Bool(x) => StaticValue::new_inline(x),
String(x) => StaticValue::new_inline(x),
PrimOp { id, arity } => StaticValue::new_primop(id, arity),
Null => StaticValue::default(),
};
self.constants.insert(val)
}
}
+18 -18
View File
@@ -3,8 +3,8 @@ use std::process::exit;
use anyhow::Result;
use clap::{Args, Parser, Subcommand};
use fix::error::Source;
use fix::runtime::Runtime;
use fix::Evaluator;
use fix_error::Source;
use hashbrown::HashSet;
use rustyline::DefaultEditor;
use rustyline::error::ReadlineError;
@@ -40,7 +40,7 @@ struct ExprSource {
file: Option<PathBuf>,
}
fn run_compile(runtime: &mut Runtime, src: ExprSource, silent: bool) -> Result<()> {
fn run_compile(eval: &mut Evaluator, src: ExprSource, _silent: bool) -> Result<()> {
let src = if let Some(expr) = src.expr {
Source::new_eval(expr)?
} else if let Some(file) = src.file {
@@ -48,11 +48,11 @@ fn run_compile(runtime: &mut Runtime, src: ExprSource, silent: bool) -> Result<(
} else {
unreachable!()
};
match runtime.compile_bytecode(src) {
Ok(ip) => {
if !silent {
println!("{}", runtime.disassemble_colored(ip));
}
match eval.compile_bytecode(src) {
Ok(_ip) => {
// if !silent {
// println!("{}", eval.disassemble_colored(ip));
// }
}
Err(err) => {
eprintln!("{:?}", miette::Report::new(*err));
@@ -62,7 +62,7 @@ fn run_compile(runtime: &mut Runtime, src: ExprSource, silent: bool) -> Result<(
Ok(())
}
fn run_eval(runtime: &mut Runtime, src: ExprSource) -> Result<()> {
fn run_eval(eval: &mut Evaluator, src: ExprSource) -> Result<()> {
let src = if let Some(expr) = src.expr {
Source::new_eval(expr)?
} else if let Some(file) = src.file {
@@ -70,7 +70,7 @@ fn run_eval(runtime: &mut Runtime, src: ExprSource) -> Result<()> {
} else {
unreachable!()
};
match runtime.eval_deep(src) {
match eval.eval_deep(src) {
Ok(value) => {
println!("{}", value.display_compat());
}
@@ -82,7 +82,7 @@ fn run_eval(runtime: &mut Runtime, src: ExprSource) -> Result<()> {
Ok(())
}
fn run_repl(runtime: &mut Runtime) -> Result<()> {
fn run_repl(eval: &mut Evaluator) -> 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]*(.*)$");
@@ -101,20 +101,20 @@ fn run_repl(runtime: &mut Runtime) -> Result<()> {
eprintln!("Error: missing expression after '='");
continue;
}
match runtime.add_binding(ident, expr, &mut scope) {
match eval.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 runtime.eval_repl(src, &scope) {
match eval.eval_repl(src, &scope) {
Ok(value) => println!("{value}"),
Err(err) => eprintln!("{:?}", miette::Report::new(*err)),
}
}
} else {
let src = Source::new_repl(line)?;
match runtime.eval_repl(src, &scope) {
match eval.eval_repl(src, &scope) {
Ok(value) => println!("{value}"),
Err(err) => eprintln!("{:?}", miette::Report::new(*err)),
}
@@ -141,11 +141,11 @@ fn main() -> Result<()> {
let cli = Cli::parse();
let mut runtime = Runtime::new()?;
let mut eval = Evaluator::new();
match cli.command {
Command::Compile { source, silent } => run_compile(&mut runtime, source, silent),
Command::Eval { source } => run_eval(&mut runtime, source),
Command::Repl => run_repl(&mut runtime),
Command::Compile { source, silent } => run_compile(&mut eval, source, silent),
Command::Eval { source } => run_eval(&mut eval, source),
Command::Repl => run_repl(&mut eval),
}
}
+1 -1
View File
@@ -4,7 +4,7 @@ use std::path::Path;
use nix_nar::Encoder;
use sha2::{Digest, Sha256};
use crate::error::{Error, Result};
use fix_error::{Error, Result};
pub fn compute_nar_hash(path: &Path) -> Result<[u8; 32]> {
let mut hasher = Sha256::new();
-559
View File
@@ -1,559 +0,0 @@
use bumpalo::Bump;
use gc_arena::{Arena, Rootable};
use ghost_cell::{GhostCell, GhostToken};
use hashbrown::{HashMap, HashSet};
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::{Ir, IrRef, RawIrRef, StringId, ThunkId};
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 maybe_thunk(&mut self, ir: IrRef<'id, 'ir>) -> IrRef<'id, 'ir> {
if !should_thunk(ir, &self.token) {
return ir;
}
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 {
sym: param_sym,
abs_layer,
} => {
if param_sym == sym {
return Ok(self.new_expr(Ir::Arg {
layer: self.thunk_scopes.len() - abs_layer,
}));
}
}
}
}
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, sym: StringId, f: F) -> R
where
F: FnOnce(&mut Self) -> R,
{
self.scopes.push(Scope::Param {
sym,
abs_layer: self.thunk_scopes.len(),
});
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>)>,
}
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: IrRef<'id, 'ir>, _token: &GhostToken<'id>) {
self.bindings.push((id, ir));
}
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 { sym: StringId, abs_layer: usize },
}
struct ScopeGuard<'a, 'ctx, 'id, 'ir> {
ctx: &'a mut DowngradeCtx<'ctx, 'id, 'ir>,
}
impl Drop for ScopeGuard<'_, '_, '_, '_> {
fn drop(&mut self) {
self.ctx.scopes.pop();
}
}
impl<'id, 'ir, 'ctx> ScopeGuard<'_, 'ctx, 'id, 'ir> {
fn as_ctx(&mut self) -> &mut DowngradeCtx<'ctx, 'id, 'ir> {
self.ctx
}
}
struct OwnedIr {
_bump: Bump,
ir: RawIrRef<'static>,
}
impl OwnedIr {
unsafe fn new(ir: RawIrRef<'_>, bump: Bump) -> Self {
Self {
_bump: bump,
ir: unsafe { std::mem::transmute::<RawIrRef<'_>, RawIrRef<'static>>(ir) },
}
}
fn as_ref(&self) -> RawIrRef<'_> {
self.ir
}
}
impl BytecodeContext for Runtime {
fn intern_string(&mut self, s: &str) -> StringId {
StringId(self.strings.get_or_intern(s))
}
fn register_span(&mut self, range: TextRange) -> u32 {
let id = self.spans.len();
let source_id = self
.sources
.len()
.checked_sub(1)
.expect("current_source not set");
self.spans.push((source_id, range));
id as u32
}
fn get_code(&self) -> &[u8] {
&self.bytecode
}
fn get_code_mut(&mut self) -> &mut Vec<u8> {
&mut self.bytecode
}
fn add_constant(&mut self, val: StaticValue) -> u32 {
let bits = val.to_bits();
*self.constant_dedup.entry(bits).or_insert_with(|| {
let idx = self.constants.len() as u32;
self.constants.push(val);
idx
})
}
}
impl DisassemblerContext for Runtime {
fn lookup_string(&self, id: u32) -> &str {
self.strings
.resolve(SymbolU32::try_from_usize(id as usize).expect("invalid string id"))
.expect("string not found")
}
fn get_code(&self) -> &[u8] {
&self.bytecode
}
}
struct WellKnownSymbols {
// TODO:
}
-410
View File
@@ -1,410 +0,0 @@
use gc_arena::{Collect, Gc, Mutation, RefLock};
use hashbrown::HashMap;
use num_enum::TryFromPrimitive;
use smallvec::SmallVec;
use string_interner::DefaultStringInterner;
use string_interner::symbol::SymbolU32;
use super::value::*;
use crate::ir::{Ir, RawIrRef, StringId};
/// Generates both the BUILTINS const table and the BuiltinId enum
/// from a single source of truth, preventing index desync.
macro_rules! define_builtins {
($(($name:literal, $variant:ident, $arity:expr)),* $(,)?) => {
/// Builtin function registry.
/// Array index IS the PrimOp id. (name, arity) pairs.
pub(crate) const BUILTINS: &[(&str, u8)] = &[
$(($name, $arity),)*
];
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, TryFromPrimitive, Collect)]
#[repr(u8)]
#[collect(require_static)]
pub(crate) enum BuiltinId {
$($variant,)*
}
};
}
define_builtins! {
("abort", Abort, 1),
("__add", Add, 2),
("__addErrorContext", AddErrorContext, 2),
("__all", All, 2),
("__any", Any, 2),
("__appendContext", AppendContext, 2),
("__attrNames", AttrNames, 1),
("__attrValues", AttrValues, 1),
("baseNameOf", BaseNameOf, 1),
("__bitAnd", BitAnd, 2),
("__bitOr", BitOr, 2),
("__bitXor", BitXor, 2),
("break", Break, 1),
("__catAttrs", CatAttrs, 2),
("__ceil", Ceil, 1),
("__compareVersions", CompareVersions, 2),
("__concatLists", ConcatLists, 1),
("__concatMap", ConcatMap, 2),
("__concatStringsSep", ConcatStringsSep, 2),
("__convertHash", ConvertHash, 1),
("__deepSeq", DeepSeq, 2),
("derivation", Derivation, 1),
("derivationStrict", DerivationStrict, 1),
("dirOf", DirOf, 1),
("__div", Div, 2),
("__elem", Elem, 2),
("__elemAt", ElemAt, 2),
("fetchGit", FetchGit, 1),
("fetchMercurial", FetchMercurial, 1),
("fetchTarball", FetchTarball, 1),
("fetchTree", FetchTree, 1),
("__fetchurl", FetchUrl, 1),
("__filter", Filter, 2),
("__filterSource", FilterSource, 2),
("__findFile", FindFile, 2),
("__floor", Floor, 1),
("__foldl'", FoldlStrict, 3),
("__fromJSON", FromJSON, 1),
("fromTOML", FromTOML, 1),
("__functionArgs", FunctionArgs, 1),
("__genList", GenList, 2),
("__genericClosure", GenericClosure, 1),
("__getAttr", GetAttr, 2),
("__getContext", GetContext, 1),
("__getEnv", GetEnv, 1),
("__groupBy", GroupBy, 2),
("__hasAttr", HasAttr, 2),
("__hasContext", HasContext, 1),
("__hashFile", HashFile, 2),
("__hashString", HashString, 2),
("__head", Head, 1),
("import", Import, 1),
("__intersectAttrs", IntersectAttrs, 2),
("__isAttrs", IsAttrs, 1),
("__isBool", IsBool, 1),
("__isFloat", IsFloat, 1),
("__isFunction", IsFunction, 1),
("__isInt", IsInt, 1),
("__isList", IsList, 1),
("isNull", IsNull, 1),
("__isPath", IsPath, 1),
("__isString", IsString, 1),
("__length", Length, 1),
("__lessThan", LessThan, 2),
("__listToAttrs", ListToAttrs, 1),
("map", Map, 2),
("__mapAttrs", MapAttrs, 2),
("__match", Match, 2),
("__mul", Mul, 2),
("null", Null, 0), // constant, not a function
("__parseDrvName", ParseDrvName, 1),
("__partition", Partition, 2),
("__path", Path, 1),
("__pathExists", PathExists, 1),
("placeholder", Placeholder, 1),
("__readDir", ReadDir, 1),
("__readFile", ReadFile, 1),
("__readFileType", ReadFileType, 1),
("removeAttrs", RemoveAttrs, 2),
("__replaceStrings", ReplaceStrings, 3),
("scopedImport", ScopedImport, 2),
("__seq", Seq, 2),
("__sort", Sort, 2),
("__split", Split, 2),
("__splitVersion", SplitVersion, 1),
("__storePath", StorePath, 1),
("__stringLength", StringLength, 1),
("__sub", Sub, 2),
("__substring", Substring, 3),
("__tail", Tail, 1),
("throw", Throw, 1),
("__toFile", ToFile, 2),
("__toJSON", ToJSON, 1),
("__toPath", ToPath, 1),
("toString", ToString, 1),
("__toXML", ToXML, 1),
("__trace", Trace, 2),
("__tryEval", TryEval, 1),
("__typeOf", TypeOf, 1),
("__unsafeDiscardStringContext", UnsafeDiscardStringContext, 1),
("__unsafeGetAttrPos", UnsafeGetAttrPos, 2),
("__warn", Warn, 2),
("__zipAttrsWith", ZipAttrsWith, 2),
}
/// Names that need to be pre-interned for builtin implementations.
const EXTRA_INTERN_NAMES: &[&str] = &[
"builtins",
"currentSystem",
"langVersion",
"nixVersion",
"storeDir",
"nixPath",
"true",
"false",
// typeOf return values
"int",
"float",
"bool",
"string",
"path",
"null",
"set",
"list",
"lambda",
// attrset keys used by builtins
"name",
"value",
"success",
"right",
"wrong",
"key",
"operator",
"startSet",
"__toString",
"outPath",
"__functor",
"drvPath",
"type",
"derivation",
"version",
];
/// Returns true if this builtin has lazy argument semantics
/// (not all args should be forced before dispatch).
pub(super) fn is_lazy_builtin(id: BuiltinId) -> bool {
matches!(
id,
BuiltinId::Seq
| BuiltinId::DeepSeq
| BuiltinId::Trace
| BuiltinId::Warn
| BuiltinId::TryEval
| BuiltinId::AddErrorContext
| BuiltinId::Break
)
}
/// Intern all builtin names and extra names needed at runtime.
fn intern_all_builtins(interner: &mut DefaultStringInterner) {
for &(name, _) in BUILTINS {
interner.get_or_intern(name);
}
for &name in EXTRA_INTERN_NAMES {
interner.get_or_intern(name);
}
}
pub(super) fn init_builtins<'gc>(
mc: &Mutation<'gc>,
strings: &mut DefaultStringInterner,
) -> (
HashMap<StringId, Ir<'static, RawIrRef<'static>>>,
Value<'gc>,
) {
let mut global_env = HashMap::new();
let builtins_sym = StringId(strings.get_or_intern("builtins"));
global_env.insert(builtins_sym, Ir::Builtins);
let mut entries = SmallVec::with_capacity(BUILTINS.len());
for (idx, &(name, arity)) in BUILTINS.iter().enumerate() {
let id = BuiltinId::try_from_primitive(idx as u8).expect("infallible");
if let Some(local_name) = name.strip_prefix("__") {
// scoped builtins
let name = StringId(strings.get_or_intern(name));
let local_name = StringId(strings.get_or_intern(local_name));
global_env.insert(name, Ir::Builtin(id));
entries.push((local_name, Value::new_inline(PrimOp { id, arity })));
} else {
// global builtins
let name = StringId(strings.get_or_intern(name));
global_env.insert(name, Ir::Builtin(id));
entries.push((name, Value::new_inline(PrimOp { id, arity })));
}
}
let consts = [
(
"__currentSystem",
Ir::BuiltinConst(StringId(strings.get_or_intern("currentSystem"))),
// FIXME: detect currentSystem
Value::new_gc(Gc::new(mc, NixString::new("x86_64-linux"))),
),
("__langVersion", Ir::Int(6), Value::new_inline(6i32)),
(
"__nixVersion",
Ir::BuiltinConst(StringId(strings.get_or_intern("nixVersion"))),
Value::new_gc(Gc::new(mc, NixString::new("2.24.0"))),
),
(
"__storeDir",
Ir::BuiltinConst(StringId(strings.get_or_intern("storeDir"))),
Value::new_gc(Gc::new(mc, NixString::new("/nix/store"))),
),
(
"__nixPath",
Ir::BuiltinConst(StringId(strings.get_or_intern("nixPath"))),
// FIXME: get from config
Value::new_gc(Gc::new(
mc,
List {
inner: SmallVec::new(),
},
)),
),
("null", Ir::Null, Value::new_inline(Null)),
("true", Ir::Bool(true), Value::new_inline(true)),
("false", Ir::Bool(false), Value::new_inline(false)),
];
for (idx, &(name, arity)) in BUILTINS.iter().enumerate() {
let id = BuiltinId::try_from_primitive(idx as u8).expect("infallible");
if let Some(local_name) = name.strip_prefix("__") {
// scoped builtins
let name = StringId(strings.get_or_intern(name));
let local_name = StringId(strings.get_or_intern(local_name));
global_env.insert(name, Ir::Builtin(id));
entries.push((local_name, Value::new_inline(PrimOp { id, arity })));
} else {
// global builtins
let name = StringId(strings.get_or_intern(name));
global_env.insert(name, Ir::Builtin(id));
entries.push((name, Value::new_inline(PrimOp { id, arity })));
}
}
for (name, ir, val) in consts {
if let Some(local_name) = name.strip_prefix("__") {
let name = StringId(strings.get_or_intern(name));
let local_name = StringId(strings.get_or_intern(local_name));
global_env.insert(name, ir);
entries.push((local_name, val));
} else {
let name = StringId(strings.get_or_intern(name));
global_env.insert(name, ir);
entries.push((name, val));
}
}
// Self-reference thunk for builtins.builtins
let self_ref_thunk: Gc<'gc, Thunk<'gc>> = Gc::new(mc, RefLock::new(ThunkState::Blackhole));
let sym = strings.get_or_intern("builtins");
entries.push((StringId(sym), Value::new_gc(self_ref_thunk)));
entries.sort_by_key(|(k, _)| *k);
let builtins_set = Gc::new(mc, unsafe { AttrSet::from_sorted_unchecked(entries) });
let builtins_val = Value::new_gc(builtins_set);
(global_env, builtins_val)
}
pub(super) type PrimOpArgs<'gc> = [Value<'gc>; 3];
pub(super) type PrimOpStrictArgs<'gc> = [StrictValue<'gc>; 3];
#[allow(non_snake_case)]
pub(super) struct WellKnownSymbols {
abort: SymbolU32,
add: SymbolU32,
addErrorContext: SymbolU32,
all: SymbolU32,
any: SymbolU32,
appendContext: SymbolU32,
attrNames: SymbolU32,
attrValues: SymbolU32,
baseNameOf: SymbolU32,
bitAnd: SymbolU32,
bitOr: SymbolU32,
bitXor: SymbolU32,
catAttrs: SymbolU32,
ceil: SymbolU32,
compareVersions: SymbolU32,
concatLists: SymbolU32,
concatMap: SymbolU32,
concatStringsSep: SymbolU32,
convertHash: SymbolU32,
deepSeq: SymbolU32,
derivation: SymbolU32,
derivationStrict: SymbolU32,
dirOf: SymbolU32,
div: SymbolU32,
elem: SymbolU32,
elemAt: SymbolU32,
fetchGit: SymbolU32,
fetchMercurial: SymbolU32,
fetchTarball: SymbolU32,
fetchTree: SymbolU32,
fetchurl: SymbolU32,
filter: SymbolU32,
filterSource: SymbolU32,
findFile: SymbolU32,
floor: SymbolU32,
foldl: SymbolU32,
fromJSON: SymbolU32,
fromTOML: SymbolU32,
functionArgs: SymbolU32,
genList: SymbolU32,
genericClosure: SymbolU32,
getAttr: SymbolU32,
getContext: SymbolU32,
getEnv: SymbolU32,
groupBy: SymbolU32,
hasAttr: SymbolU32,
hasContext: SymbolU32,
hashFile: SymbolU32,
hashString: SymbolU32,
head: SymbolU32,
import: SymbolU32,
intersectAttrs: SymbolU32,
isAttrs: SymbolU32,
isBool: SymbolU32,
isFloat: SymbolU32,
isFunction: SymbolU32,
isInt: SymbolU32,
isList: SymbolU32,
isNull: SymbolU32,
isPath: SymbolU32,
isString: SymbolU32,
length: SymbolU32,
lessThan: SymbolU32,
listToAttrs: SymbolU32,
map: SymbolU32,
mapAttrs: SymbolU32,
match_: SymbolU32,
mul: SymbolU32,
null: SymbolU32,
parseDrvName: SymbolU32,
partition: SymbolU32,
path: SymbolU32,
pathExists: SymbolU32,
placeholder: SymbolU32,
readDir: SymbolU32,
readFile: SymbolU32,
readFileType: SymbolU32,
removeAttrs: SymbolU32,
replaceStrings: SymbolU32,
scopedImport: SymbolU32,
seq: SymbolU32,
sort: SymbolU32,
split: SymbolU32,
splitVersion: SymbolU32,
storePath: SymbolU32,
stringLength: SymbolU32,
sub: SymbolU32,
substring: SymbolU32,
tail: SymbolU32,
throw: SymbolU32,
toFile: SymbolU32,
toJSON: SymbolU32,
toPath: SymbolU32,
toString: SymbolU32,
toXML: SymbolU32,
trace: SymbolU32,
tryEval: SymbolU32,
typeOf: SymbolU32,
unsafeDiscardStringContext: SymbolU32,
unsafeGetAttrPos: SymbolU32,
warn: SymbolU32,
zipAttrsWith: SymbolU32,
break_: SymbolU32,
}
-31
View File
@@ -1,31 +0,0 @@
drvAttrs@{
outputs ? [ "out" ],
...
}:
let
strict = derivationStrict drvAttrs;
commonAttrs =
drvAttrs
// (builtins.listToAttrs outputsList)
// {
all = map (x: x.value) outputsList;
inherit drvAttrs;
};
outputToAttrListElement = outputName: {
name = outputName;
value = commonAttrs // {
outPath = builtins.getAttr outputName strict;
drvPath = strict.drvPath;
type = "derivation";
inherit outputName;
};
};
outputsList = map outputToAttrListElement outputs;
in
(builtins.head outputsList).value
-76
View File
@@ -1,76 +0,0 @@
{
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; })
)
+1 -1
View File
@@ -1,4 +1,4 @@
use fix::value::Value;
use fix_common::Value;
use crate::utils::{eval_deep, eval_deep_result};
+6 -6
View File
@@ -1,6 +1,6 @@
use fix::error::Source;
use fix::runtime::Runtime;
use fix::value::Value;
use fix::Evaluator;
use fix_common::Value;
use fix_error::Source;
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 = Runtime::new().unwrap();
let mut ctx = Evaluator::new();
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 = Runtime::new().unwrap();
let mut ctx = Evaluator::new();
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 = Runtime::new().unwrap();
let mut ctx = Evaluator::new();
let temp_dir = tempfile::tempdir().unwrap();
let test_file = temp_dir.path().join("flat.txt");
std::fs::write(&test_file, "Flat content").unwrap();
+5 -5
View File
@@ -2,9 +2,9 @@
use std::path::PathBuf;
use fix::error::Source;
use fix::runtime::Runtime;
use fix::value::Value;
use fix::Evaluator;
use fix_common::Value;
use fix_error::{Source, SourceType};
fn get_lang_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/tests/lang")
@@ -14,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 = Runtime::new().map_err(|e| e.to_string())?;
let mut ctx = Evaluator::new();
let content = std::fs::read_to_string(&nix_path).unwrap();
let source = Source {
ty: fix::error::SourceType::File(nix_path.into()),
ty: SourceType::File(nix_path.into()),
src: content.into(),
};
ctx.eval_deep(source.clone())
+5 -5
View File
@@ -1,5 +1,5 @@
use fix::runtime::Runtime;
use fix::value::Value;
use fix::Evaluator;
use fix_common::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 = Runtime::new().unwrap();
let mut rt = Evaluator::new();
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 = Runtime::new().unwrap();
let mut rt = Evaluator::new();
let result = rt
.eval(
r#"
@@ -224,7 +224,7 @@ fn tostring_preserves_context() {
#[test_log::test]
fn interpolation_derivation_returns_outpath() {
let mut rt = Runtime::new().unwrap();
let mut rt = Evaluator::new();
let result = rt
.eval(
r#"
+8 -15
View File
@@ -1,38 +1,31 @@
#![allow(dead_code)]
use fix::error::{Result, Source};
use fix::runtime::Runtime;
use fix::value::Value;
use fix::Evaluator;
use fix_common::Value;
use fix_error::{Result, Source};
pub fn eval(expr: &str) -> Value {
Runtime::new()
.unwrap()
Evaluator::new()
.eval(Source::new_eval(expr.into()).unwrap())
.unwrap()
}
pub fn eval_shallow(expr: &str) -> Value {
Runtime::new()
.unwrap()
Evaluator::new()
.eval_shallow(Source::new_eval(expr.into()).unwrap())
.unwrap()
}
pub fn eval_deep(expr: &str) -> Value {
Runtime::new()
.unwrap()
Evaluator::new()
.eval_deep(Source::new_eval(expr.into()).unwrap())
.unwrap()
}
pub fn eval_deep_result(expr: &str) -> Result<Value> {
Runtime::new()
.unwrap()
.eval_deep(Source::new_eval(expr.into()).unwrap())
Evaluator::new().eval_deep(Source::new_eval(expr.into()).unwrap())
}
pub fn eval_result(expr: &str) -> Result<Value> {
Runtime::new()
.unwrap()
.eval(Source::new_eval(expr.into()).unwrap())
Evaluator::new().eval(Source::new_eval(expr.into()).unwrap())
}