Files
nix-js/nix-js/src/context.rs

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 })
}
}