598 lines
17 KiB
Rust
598 lines
17 KiB
Rust
use std::path::Path;
|
|
use std::ptr::NonNull;
|
|
|
|
use hashbrown::{HashMap, HashSet};
|
|
use itertools::Itertools as _;
|
|
use petgraph::graphmap::DiGraphMap;
|
|
use rnix::TextRange;
|
|
use string_interner::DefaultStringInterner;
|
|
|
|
use crate::codegen::{CodegenContext, compile};
|
|
use crate::error::{Error, Result, Source};
|
|
use crate::ir::{
|
|
Arg, ArgId, Bool, Builtin, Downgrade as _, DowngradeContext, ExprId, ExprRef, Ir, Null, SymId,
|
|
Thunk, ToIr as _, synthetic_span,
|
|
};
|
|
use crate::runtime::{Runtime, RuntimeContext};
|
|
use crate::store::{Store, StoreBackend, StoreConfig};
|
|
use crate::value::Value;
|
|
|
|
#[derive(Debug)]
|
|
pub(crate) struct SccInfo {
|
|
/// list of SCCs (exprs, recursive)
|
|
pub(crate) sccs: Vec<(Vec<ExprId>, bool)>,
|
|
}
|
|
|
|
pub struct Context {
|
|
ctx: Ctx,
|
|
runtime: Runtime<Ctx>,
|
|
}
|
|
|
|
impl Context {
|
|
pub fn new() -> Result<Self> {
|
|
let ctx = Ctx::new()?;
|
|
let runtime = Runtime::new()?;
|
|
|
|
Ok(Self { ctx, runtime })
|
|
}
|
|
|
|
pub fn eval_code(&mut self, source: Source) -> Result<Value> {
|
|
tracing::info!("Starting evaluation");
|
|
|
|
tracing::debug!("Compiling code");
|
|
let code = self.compile_code(source)?;
|
|
|
|
tracing::debug!("Executing JavaScript");
|
|
self.runtime
|
|
.eval(format!("Nix.force({code})"), &mut self.ctx)
|
|
}
|
|
|
|
pub fn compile_code(&mut self, source: Source) -> Result<String> {
|
|
self.ctx.compile_code(source)
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
pub(crate) fn eval_js(&mut self, code: String) -> Result<Value> {
|
|
self.runtime.eval(code, &mut self.ctx)
|
|
}
|
|
|
|
pub fn get_store_dir(&self) -> &str {
|
|
self.ctx.get_store_dir()
|
|
}
|
|
}
|
|
|
|
pub(crate) struct Ctx {
|
|
irs: Vec<Ir>,
|
|
symbols: DefaultStringInterner,
|
|
global: NonNull<HashMap<SymId, ExprId>>,
|
|
sources: Vec<Source>,
|
|
store: StoreBackend,
|
|
}
|
|
|
|
impl Ctx {
|
|
fn new() -> Result<Self> {
|
|
use crate::ir::{Builtins, ToIr as _};
|
|
|
|
let mut symbols = DefaultStringInterner::new();
|
|
let mut irs = Vec::new();
|
|
let mut global = HashMap::new();
|
|
|
|
irs.push(
|
|
Builtins {
|
|
span: synthetic_span(),
|
|
}
|
|
.to_ir(),
|
|
);
|
|
let builtins_expr = ExprId(0);
|
|
|
|
let builtins_sym = symbols.get_or_intern("builtins");
|
|
global.insert(builtins_sym, builtins_expr);
|
|
|
|
let free_globals = [
|
|
"abort",
|
|
"baseNameOf",
|
|
"break",
|
|
"dirOf",
|
|
"derivation",
|
|
"derivationStrict",
|
|
"fetchGit",
|
|
"fetchMercurial",
|
|
"fetchTarball",
|
|
"fetchTree",
|
|
"fromTOML",
|
|
"import",
|
|
"isNull",
|
|
"map",
|
|
"placeholder",
|
|
"removeAttrs",
|
|
"scopedImport",
|
|
"throw",
|
|
"toString",
|
|
];
|
|
let consts = [
|
|
(
|
|
"true",
|
|
Bool {
|
|
inner: true,
|
|
span: synthetic_span(),
|
|
}
|
|
.to_ir(),
|
|
),
|
|
(
|
|
"false",
|
|
Bool {
|
|
inner: false,
|
|
span: synthetic_span(),
|
|
}
|
|
.to_ir(),
|
|
),
|
|
(
|
|
"null",
|
|
Null {
|
|
span: synthetic_span(),
|
|
}
|
|
.to_ir(),
|
|
),
|
|
];
|
|
|
|
for name in free_globals {
|
|
let name_sym = symbols.get_or_intern(name);
|
|
let id = ExprId(irs.len());
|
|
irs.push(
|
|
Builtin {
|
|
inner: name_sym,
|
|
span: synthetic_span(),
|
|
}
|
|
.to_ir(),
|
|
);
|
|
global.insert(name_sym, id);
|
|
}
|
|
for (name, value) in consts {
|
|
let name_sym = symbols.get_or_intern(name);
|
|
let id = ExprId(irs.len());
|
|
irs.push(value);
|
|
global.insert(name_sym, id);
|
|
}
|
|
|
|
let config = StoreConfig::from_env();
|
|
let store = StoreBackend::new(config)?;
|
|
|
|
Ok(Self {
|
|
symbols,
|
|
irs,
|
|
global: unsafe { NonNull::new_unchecked(Box::leak(Box::new(global))) },
|
|
sources: Vec::new(),
|
|
store,
|
|
})
|
|
}
|
|
|
|
pub(crate) fn downgrade_ctx<'a>(&'a mut self) -> DowngradeCtx<'a> {
|
|
let global_ref = unsafe { self.global.as_ref() };
|
|
DowngradeCtx::new(self, global_ref)
|
|
}
|
|
|
|
pub(crate) fn get_current_dir(&self) -> &Path {
|
|
self.sources
|
|
.last()
|
|
.as_ref()
|
|
.expect("current_source is not set")
|
|
.get_dir()
|
|
}
|
|
|
|
pub(crate) fn get_current_source(&self) -> Source {
|
|
self.sources
|
|
.last()
|
|
.expect("current_source is not set")
|
|
.clone()
|
|
}
|
|
|
|
pub(crate) fn get_source(&self, id: usize) -> Source {
|
|
self.sources.get(id).expect("source not found").clone()
|
|
}
|
|
|
|
fn compile_code(&mut self, source: Source) -> Result<String> {
|
|
tracing::debug!("Parsing Nix expression");
|
|
|
|
self.sources.push(source.clone());
|
|
|
|
let root = rnix::Root::parse(&source.src);
|
|
if !root.errors().is_empty() {
|
|
let error_msg = root.errors().iter().join("; ");
|
|
let err = Error::parse_error(error_msg).with_source(source);
|
|
return Err(err);
|
|
}
|
|
|
|
#[allow(clippy::unwrap_used)]
|
|
let root = self
|
|
.downgrade_ctx()
|
|
.downgrade(root.tree().expr().unwrap())?;
|
|
|
|
tracing::debug!("Generating JavaScript code");
|
|
let code = compile(self.get_ir(root), self);
|
|
tracing::debug!("Generated code: {}", &code);
|
|
Ok(code)
|
|
}
|
|
}
|
|
|
|
impl CodegenContext for Ctx {
|
|
fn get_ir(&self, id: ExprId) -> &Ir {
|
|
self.irs.get(id.0).expect("ExprId out of bounds")
|
|
}
|
|
fn get_sym(&self, id: SymId) -> &str {
|
|
self.symbols.resolve(id).expect("SymId out of bounds")
|
|
}
|
|
fn get_current_dir(&self) -> &std::path::Path {
|
|
self.get_current_dir()
|
|
}
|
|
fn get_current_source_id(&self) -> usize {
|
|
self.sources
|
|
.len()
|
|
.checked_sub(1)
|
|
.expect("current_source not set")
|
|
}
|
|
fn get_current_source(&self) -> crate::error::Source {
|
|
self.sources.last().expect("current_source not set").clone()
|
|
}
|
|
fn get_store_dir(&self) -> &str {
|
|
self.store.as_store().get_store_dir()
|
|
}
|
|
}
|
|
|
|
impl RuntimeContext for Ctx {
|
|
fn get_current_dir(&self) -> &Path {
|
|
self.get_current_dir()
|
|
}
|
|
fn add_source(&mut self, source: Source) {
|
|
self.sources.push(source);
|
|
}
|
|
fn compile_code(&mut self, source: Source) -> Result<String> {
|
|
self.compile_code(source)
|
|
}
|
|
fn get_source(&self, id: usize) -> Source {
|
|
self.get_source(id)
|
|
}
|
|
fn get_store(&self) -> &dyn Store {
|
|
self.store.as_store()
|
|
}
|
|
}
|
|
|
|
struct DependencyTracker {
|
|
graph: DiGraphMap<ExprId, ()>,
|
|
current_binding: Option<ExprId>,
|
|
let_scope_exprs: HashSet<ExprId>,
|
|
// The outer binding that owns this tracker (for nested let scopes in function params)
|
|
owner_binding: Option<ExprId>,
|
|
}
|
|
|
|
enum Scope<'ctx> {
|
|
Global(&'ctx HashMap<SymId, ExprId>),
|
|
Let(HashMap<SymId, ExprId>),
|
|
Param(SymId, ExprId),
|
|
With(ExprId),
|
|
}
|
|
|
|
struct ScopeGuard<'a, 'ctx> {
|
|
ctx: &'a mut DowngradeCtx<'ctx>,
|
|
}
|
|
|
|
impl<'a, 'ctx> Drop for ScopeGuard<'a, 'ctx> {
|
|
fn drop(&mut self) {
|
|
self.ctx.scopes.pop();
|
|
}
|
|
}
|
|
|
|
impl<'a, 'ctx> ScopeGuard<'a, 'ctx> {
|
|
fn as_ctx(&mut self) -> &mut DowngradeCtx<'ctx> {
|
|
self.ctx
|
|
}
|
|
}
|
|
|
|
pub struct DowngradeCtx<'ctx> {
|
|
ctx: &'ctx mut Ctx,
|
|
irs: Vec<Option<Ir>>,
|
|
scopes: Vec<Scope<'ctx>>,
|
|
arg_id: usize,
|
|
dep_tracker_stack: Vec<DependencyTracker>,
|
|
}
|
|
|
|
impl<'ctx> DowngradeCtx<'ctx> {
|
|
fn new(ctx: &'ctx mut Ctx, global: &'ctx HashMap<SymId, ExprId>) -> Self {
|
|
Self {
|
|
scopes: vec![Scope::Global(global)],
|
|
irs: vec![],
|
|
arg_id: 0,
|
|
dep_tracker_stack: Vec::new(),
|
|
ctx,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl DowngradeContext for DowngradeCtx<'_> {
|
|
fn new_expr(&mut self, expr: Ir) -> ExprId {
|
|
self.irs.push(Some(expr));
|
|
ExprId(self.ctx.irs.len() + self.irs.len() - 1)
|
|
}
|
|
|
|
fn new_arg(&mut self, span: TextRange) -> ExprId {
|
|
self.irs.push(Some(
|
|
Arg {
|
|
inner: ArgId(self.arg_id),
|
|
span,
|
|
}
|
|
.to_ir(),
|
|
));
|
|
self.arg_id += 1;
|
|
ExprId(self.ctx.irs.len() + self.irs.len() - 1)
|
|
}
|
|
|
|
fn get_ir(&self, id: ExprId) -> &Ir {
|
|
if id.0 < self.ctx.irs.len() {
|
|
self.ctx.irs.get(id.0).expect("unreachable")
|
|
} else {
|
|
self.irs
|
|
.get(id.0 - self.ctx.irs.len())
|
|
.expect("ExprId out of bounds")
|
|
.as_ref()
|
|
.expect("maybe_thunk called on an extracted expr")
|
|
}
|
|
}
|
|
|
|
fn maybe_thunk(&mut self, id: ExprId) -> ExprId {
|
|
let ir = self.get_ir(id);
|
|
match ir {
|
|
Ir::Builtin(_)
|
|
| Ir::Builtins(_)
|
|
| Ir::Int(_)
|
|
| Ir::Float(_)
|
|
| Ir::Bool(_)
|
|
| Ir::Null(_)
|
|
| Ir::Str(_) => id,
|
|
_ => self.new_expr(
|
|
Thunk {
|
|
inner: id,
|
|
span: ir.span(),
|
|
}
|
|
.to_ir(),
|
|
),
|
|
}
|
|
}
|
|
|
|
fn new_sym(&mut self, sym: String) -> SymId {
|
|
self.ctx.symbols.get_or_intern(sym)
|
|
}
|
|
|
|
fn get_sym(&self, id: SymId) -> &str {
|
|
self.ctx.get_sym(id)
|
|
}
|
|
|
|
fn lookup(&mut self, sym: SymId, span: TextRange) -> Result<ExprId> {
|
|
for scope in self.scopes.iter().rev() {
|
|
match scope {
|
|
&Scope::Global(global_scope) => {
|
|
if let Some(&expr) = global_scope.get(&sym) {
|
|
return Ok(expr);
|
|
}
|
|
}
|
|
Scope::Let(let_scope) => {
|
|
if let Some(&expr) = let_scope.get(&sym) {
|
|
// Find which tracker contains this expression
|
|
let expr_tracker_idx = self
|
|
.dep_tracker_stack
|
|
.iter()
|
|
.position(|t| t.let_scope_exprs.contains(&expr));
|
|
|
|
// Find the innermost tracker with a current_binding
|
|
let current_tracker_idx = self
|
|
.dep_tracker_stack
|
|
.iter()
|
|
.rposition(|t| t.current_binding.is_some());
|
|
|
|
// Record dependency if both exist
|
|
if let (Some(expr_idx), Some(curr_idx)) =
|
|
(expr_tracker_idx, current_tracker_idx)
|
|
{
|
|
let current_binding = self.dep_tracker_stack[curr_idx]
|
|
.current_binding
|
|
.expect("current_binding not set");
|
|
let owner_binding = self.dep_tracker_stack[curr_idx].owner_binding;
|
|
|
|
// If referencing from inner scope to outer scope
|
|
if curr_idx >= expr_idx {
|
|
let tracker = &mut self.dep_tracker_stack[expr_idx];
|
|
let from_node = current_binding;
|
|
let to_node = expr;
|
|
if curr_idx > expr_idx {
|
|
// Cross-scope reference: use owner_binding if available
|
|
if let Some(owner) = owner_binding {
|
|
tracker.graph.add_edge(owner, expr, ());
|
|
}
|
|
} else {
|
|
// Same-level reference: record directly
|
|
tracker.graph.add_edge(from_node, to_node, ());
|
|
}
|
|
}
|
|
}
|
|
|
|
return Ok(self.new_expr(ExprRef { inner: expr, span }.to_ir()));
|
|
}
|
|
}
|
|
&Scope::Param(param_sym, expr) => {
|
|
if param_sym == sym {
|
|
return Ok(expr);
|
|
}
|
|
}
|
|
&Scope::With(_) => (),
|
|
}
|
|
}
|
|
|
|
let namespaces: Vec<ExprId> = self
|
|
.scopes
|
|
.iter()
|
|
.filter_map(|scope| {
|
|
if let &Scope::With(namespace) = scope {
|
|
Some(namespace)
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.collect();
|
|
let mut result = None;
|
|
for namespace in namespaces {
|
|
use crate::ir::{Attr, Select};
|
|
let select = Select {
|
|
expr: namespace,
|
|
attrpath: vec![Attr::Str(sym, synthetic_span())],
|
|
default: result, // Link to outer With or None
|
|
span,
|
|
};
|
|
result = Some(self.new_expr(select.to_ir()));
|
|
}
|
|
result.ok_or_else(|| {
|
|
Error::downgrade_error(
|
|
format!("'{}' not found", self.get_sym(sym)),
|
|
self.get_current_source(),
|
|
span,
|
|
)
|
|
})
|
|
}
|
|
|
|
fn extract_ir(&mut self, id: ExprId) -> Ir {
|
|
let local_id = id.0 - self.ctx.irs.len();
|
|
self.irs
|
|
.get_mut(local_id)
|
|
.expect("ExprId out of bounds")
|
|
.take()
|
|
.expect("extract_expr called on an already extracted expr")
|
|
}
|
|
|
|
fn replace_ir(&mut self, id: ExprId, expr: Ir) {
|
|
let local_id = id.0 - self.ctx.irs.len();
|
|
let _ = self
|
|
.irs
|
|
.get_mut(local_id)
|
|
.expect("ExprId out of bounds")
|
|
.insert(expr);
|
|
}
|
|
|
|
fn get_current_source(&self) -> Source {
|
|
self.ctx.get_current_source()
|
|
}
|
|
|
|
#[allow(refining_impl_trait)]
|
|
fn reserve_slots(&mut self, slots: usize) -> impl Iterator<Item = ExprId> + Clone + use<> {
|
|
let start = self.ctx.irs.len() + self.irs.len();
|
|
self.irs.extend(std::iter::repeat_with(|| None).take(slots));
|
|
(start..start + slots).map(ExprId)
|
|
}
|
|
|
|
fn downgrade(mut self, root: rnix::ast::Expr) -> Result<ExprId> {
|
|
let root = root.downgrade(&mut self)?;
|
|
self.ctx
|
|
.irs
|
|
.extend(self.irs.into_iter().map(Option::unwrap));
|
|
Ok(root)
|
|
}
|
|
|
|
fn with_let_scope<F, R>(&mut self, bindings: HashMap<SymId, ExprId>, f: F) -> R
|
|
where
|
|
F: FnOnce(&mut Self) -> R,
|
|
{
|
|
self.scopes.push(Scope::Let(bindings));
|
|
let mut guard = ScopeGuard { ctx: self };
|
|
f(guard.as_ctx())
|
|
}
|
|
|
|
fn with_param_scope<F, R>(&mut self, param: SymId, arg: ExprId, 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, namespace: ExprId, f: F) -> R
|
|
where
|
|
F: FnOnce(&mut Self) -> R,
|
|
{
|
|
self.scopes.push(Scope::With(namespace));
|
|
let mut guard = ScopeGuard { ctx: self };
|
|
f(guard.as_ctx())
|
|
}
|
|
|
|
fn push_dep_tracker(&mut self, slots: &[ExprId]) {
|
|
let mut graph = DiGraphMap::new();
|
|
let mut let_scope_exprs = HashSet::new();
|
|
|
|
for &expr in slots.iter() {
|
|
graph.add_node(expr);
|
|
let_scope_exprs.insert(expr);
|
|
}
|
|
|
|
self.dep_tracker_stack.push(DependencyTracker {
|
|
graph,
|
|
current_binding: None,
|
|
let_scope_exprs,
|
|
owner_binding: None,
|
|
});
|
|
}
|
|
|
|
fn push_dep_tracker_with_owner(&mut self, slots: &[ExprId], owner: ExprId) {
|
|
let mut graph = DiGraphMap::new();
|
|
let mut let_scope_exprs = HashSet::new();
|
|
|
|
for &expr in slots.iter() {
|
|
graph.add_node(expr);
|
|
let_scope_exprs.insert(expr);
|
|
}
|
|
|
|
self.dep_tracker_stack.push(DependencyTracker {
|
|
graph,
|
|
current_binding: None,
|
|
let_scope_exprs,
|
|
owner_binding: Some(owner),
|
|
});
|
|
}
|
|
|
|
fn get_current_binding(&self) -> Option<ExprId> {
|
|
self.dep_tracker_stack
|
|
.last()
|
|
.and_then(|t| t.current_binding)
|
|
}
|
|
|
|
fn set_current_binding(&mut self, expr: Option<ExprId>) {
|
|
if let Some(tracker) = self.dep_tracker_stack.last_mut() {
|
|
tracker.current_binding = expr;
|
|
}
|
|
}
|
|
|
|
fn pop_dep_tracker(&mut self) -> Result<SccInfo> {
|
|
let tracker = self
|
|
.dep_tracker_stack
|
|
.pop()
|
|
.expect("pop_dep_tracker without active tracker");
|
|
|
|
use petgraph::algo::kosaraju_scc;
|
|
let sccs = kosaraju_scc(&tracker.graph);
|
|
|
|
let mut sccs_topo = Vec::new();
|
|
|
|
for scc_nodes in sccs.iter() {
|
|
let mut scc_exprs = Vec::new();
|
|
let mut is_recursive = scc_nodes.len() > 1;
|
|
|
|
for &expr in scc_nodes {
|
|
scc_exprs.push(expr);
|
|
|
|
if !is_recursive && tracker.graph.contains_edge(expr, expr) {
|
|
is_recursive = true;
|
|
}
|
|
}
|
|
|
|
sccs_topo.push((scc_exprs, is_recursive));
|
|
}
|
|
|
|
Ok(SccInfo { sccs: sccs_topo })
|
|
}
|
|
}
|