Compare commits

..

7 Commits

430 changed files with 5911 additions and 451 deletions

View File

@@ -4,3 +4,7 @@ members = [
"nix-js",
"nix-js-macros"
]
[profile.profiling]
inherits = "release"
debug = true

View File

@@ -31,6 +31,7 @@
valgrind
hyperfine
just
samply
nodejs
nodePackages.npm

View File

@@ -92,6 +92,7 @@ pub fn ir_impl(input: TokenStream) -> TokenStream {
let mut mut_variants = Vec::new();
let mut as_ref_arms = Vec::new();
let mut as_mut_arms = Vec::new();
let mut span_arms = Vec::new();
let mut from_impls = Vec::new();
let mut to_trait_impls = Vec::new();
@@ -112,6 +113,7 @@ pub fn ir_impl(input: TokenStream) -> TokenStream {
mut_variants.push(quote! { #name(&'a mut #inner_type) });
as_ref_arms.push(quote! { Self::#name(inner) => #ref_name::#name(inner) });
as_mut_arms.push(quote! { Self::#name(inner) => #mut_name::#name(inner) });
span_arms.push(quote! { Self::#name(inner) => inner.span });
from_impls.push(quote! {
impl From<#inner_type> for #base_name {
fn from(val: #inner_type) -> Self { #base_name::#name(val) }
@@ -140,6 +142,7 @@ pub fn ir_impl(input: TokenStream) -> TokenStream {
mut_variants.push(quote! { #name(&'a mut #inner_type) });
as_ref_arms.push(quote! { Self::#name(inner) => #ref_name::#name(inner) });
as_mut_arms.push(quote! { Self::#name(inner) => #mut_name::#name(inner) });
span_arms.push(quote! { Self::#name(inner) => inner.span });
from_impls.push(quote! {
impl From<#inner_type> for #base_name {
fn from(val: #inner_type) -> Self { #base_name::#name(val) }
@@ -172,6 +175,7 @@ pub fn ir_impl(input: TokenStream) -> TokenStream {
mut_variants.push(quote! { #name(&'a mut #inner_type) });
as_ref_arms.push(quote! { Self::#name(inner) => #ref_name::#name(inner) });
as_mut_arms.push(quote! { Self::#name(inner) => #mut_name::#name(inner) });
span_arms.push(quote! { Self::#name(inner) => inner.span });
from_impls.push(quote! {
impl From<#inner_type> for #base_name {
fn from(val: #inner_type) -> Self { #base_name::#name(val) }
@@ -223,6 +227,12 @@ pub fn ir_impl(input: TokenStream) -> TokenStream {
#( #as_mut_arms ),*
}
}
pub fn span(&self) -> rnix::TextRange {
match self {
#( #span_arms ),*
}
}
}
// `From` implementations for converting variant structs into the main enum.

View File

@@ -365,65 +365,52 @@ const specialAttrs = new Set([
export const derivation = (args: NixValue): NixAttrs => {
const attrs = forceAttrs(args);
const strict = derivationStrict(args);
const outputs: string[] = extractOutputs(attrs);
const drvName = validateName(attrs);
const collectedContext: NixStringContext = new Set();
const builder = validateBuilder(attrs, collectedContext);
const platform = validateSystem(attrs);
const structuredAttrs = "__structuredAttrs" in attrs ? force(attrs.__structuredAttrs) === true : false;
const ignoreNulls = "__ignoreNulls" in attrs ? force(attrs.__ignoreNulls) === true : false;
const drvArgs = extractArgs(attrs, collectedContext);
const baseAttrs: NixAttrs = {
const strictThunk = createThunk(() => derivationStrict(args), "derivationStrict");
const commonAttrs: NixAttrs = { ...attrs };
const outputToAttrListElement = (outputName: string): { name: string; value: NixAttrs } => {
const value: NixAttrs = {
...commonAttrs,
outPath: createThunk(() => (force(strictThunk) as NixAttrs)[outputName], `outPath_${outputName}`),
drvPath: createThunk(() => (force(strictThunk) as NixAttrs).drvPath, "drvPath"),
type: "derivation",
drvPath: strict.drvPath,
name: drvName,
builder,
system: platform,
};
if (drvArgs.length > 0) {
baseAttrs.args = drvArgs;
}
if (!structuredAttrs) {
for (const [key, value] of Object.entries(attrs)) {
if (!specialAttrs.has(key) && !outputs.includes(key)) {
const forcedValue = force(value);
if (!(ignoreNulls && forcedValue === null)) {
baseAttrs[key] = value;
}
}
}
}
const outputsList: NixAttrs[] = [];
for (const outputName of outputs) {
const outputObj: NixAttrs = {
...baseAttrs,
outPath: strict[outputName],
outputName,
};
outputsList.push(outputObj);
}
return { name: outputName, value };
};
baseAttrs.drvAttrs = attrs;
for (const [i, outputName] of outputs.entries()) {
baseAttrs[outputName] = createThunk(() => outputsList[i], `output_${outputName}`);
}
baseAttrs.all = createThunk(() => outputsList, "all_outputs");
const outputsList = outputs.map(outputToAttrListElement);
for (const outputObj of outputsList) {
for (const { name: outputName, value } of outputsList) {
commonAttrs[outputName] = createThunk(
() => outputsList.find((o) => o.name === outputName)!.value,
`output_${outputName}`,
);
}
commonAttrs.all = createThunk(
() => outputsList.map((o) => o.value),
"all_outputs",
);
commonAttrs.drvAttrs = attrs;
for (const { value: outputObj } of outputsList) {
for (const { name: outputName } of outputsList) {
outputObj[outputName] = createThunk(
() => outputsList.find((o) => o.name === outputName)!.value,
`output_${outputName}`,
);
}
outputObj.all = createThunk(
() => outputsList.map((o) => o.value),
"all_outputs",
);
outputObj.drvAttrs = attrs;
for (const [i, outputName] of outputs.entries()) {
outputObj[outputName] = createThunk(() => outputsList[i], `output_${outputName}`);
}
outputObj.all = createThunk(() => outputsList, "all_outputs");
}
return outputsList[0];
return outputsList[0].value;
};

View File

@@ -141,10 +141,9 @@ export const all =
export const any =
(pred: NixValue) =>
(list: NixValue): boolean => {
const forcedList = forceList(list);
if (forcedList.length) {
// CppNix forces `pred` eagerly
const f = forceFunction(pred);
const forcedList = forceList(list);
// `false` when no element
return forcedList.some((e) => forceBool(f(e)));
}
return true;
};

View File

@@ -4,7 +4,7 @@
* All functionality is exported via the global `Nix` object
*/
import { createThunk, force, isThunk, IS_THUNK, DEBUG_THUNKS } from "./thunk";
import { createThunk, force, isThunk, IS_THUNK, DEBUG_THUNKS, forceDeepSafe, IS_CYCLE } from "./thunk";
import {
select,
selectWithDefault,
@@ -34,9 +34,11 @@ export type NixRuntime = typeof Nix;
export const Nix = {
createThunk,
force,
forceDeepSafe,
forceBool,
isThunk,
IS_THUNK,
IS_CYCLE,
HAS_CONTEXT,
IS_PATH,
DEBUG_THUNKS,

View File

@@ -4,6 +4,8 @@
*/
import type { NixValue, NixThunkInterface, NixStrictValue } from "./types";
import { HAS_CONTEXT } from "./string-context";
import { IS_PATH } from "./types";
/**
* Symbol used to mark objects as thunks
@@ -132,3 +134,50 @@ export const force = (value: NixValue): NixStrictValue => {
export const createThunk = (func: () => NixValue, label?: string): NixThunkInterface => {
return new NixThunk(func, label);
};
/**
* Symbol to mark cyclic references detected during deep forcing
*/
export const IS_CYCLE = Symbol("is_cycle");
/**
* Marker object for cyclic references
*/
export const CYCLE_MARKER = { [IS_CYCLE]: true };
/**
* Deeply force a value, handling cycles by returning a special marker.
* Uses WeakSet to track seen objects and avoid infinite recursion.
* Returns a fully forced value where thunks are replaced with their results.
* Cyclic references are replaced with CYCLE_MARKER.
*/
export const forceDeepSafe = (value: NixValue, seen: WeakSet<object> = new WeakSet()): NixStrictValue => {
const forced = force(value);
if (forced === null || typeof forced !== "object") {
return forced;
}
if (seen.has(forced)) {
return CYCLE_MARKER;
}
seen.add(forced);
if (HAS_CONTEXT in forced || IS_PATH in forced) {
return forced;
}
if (Array.isArray(forced)) {
return forced.map((item) => forceDeepSafe(item, seen));
}
if (typeof forced === "object") {
const result: Record<string, NixValue> = {};
for (const [key, val] of Object.entries(forced)) {
result[key] = forceDeepSafe(val, seen);
}
return result;
}
return forced;
};

View File

@@ -351,7 +351,7 @@ impl<Ctx: CodegenContext> Compile<Ctx> for BinOp {
Impl => {
code!(
buf, ctx;
"Nix.withContext(\"while evaluating the || operator\"," self.span ",()=>(Nix.forceBool(" lhs ")||Nix.forceBool(" rhs ")))"
"Nix.withContext(\"while evaluating the -> operator\"," self.span ",()=>(!Nix.forceBool(" lhs ")||Nix.forceBool(" rhs ")))"
);
}
PipeL => {

View File

@@ -37,7 +37,7 @@ impl Context {
tracing::debug!("Executing JavaScript");
self.runtime
.eval(format!("Nix.force({code})"), &mut self.ctx)
.eval(format!("Nix.forceDeepSafe({code})"), &mut self.ctx)
}
pub fn compile_code(&mut self, source: Source) -> Result<String> {
@@ -274,7 +274,7 @@ impl<'a, 'ctx> ScopeGuard<'a, 'ctx> {
pub struct DowngradeCtx<'ctx> {
ctx: &'ctx mut Ctx,
irs: Vec<Option<Ir>>,
irs: Vec<Ir>,
scopes: Vec<Scope<'ctx>>,
arg_id: usize,
thunk_scopes: Vec<Vec<(ExprId, ExprId)>>,
@@ -294,18 +294,18 @@ impl<'ctx> DowngradeCtx<'ctx> {
impl DowngradeContext for DowngradeCtx<'_> {
fn new_expr(&mut self, expr: Ir) -> ExprId {
self.irs.push(Some(expr));
self.irs.push(expr);
ExprId(self.ctx.irs.len() + self.irs.len() - 1)
}
fn new_arg(&mut self, span: TextRange) -> ExprId {
self.irs.push(Some(
self.irs.push(
Arg {
inner: ArgId(self.arg_id),
span,
}
.to_ir(),
));
);
self.arg_id += 1;
ExprId(self.ctx.irs.len() + self.irs.len() - 1)
}
@@ -317,8 +317,6 @@ impl DowngradeContext for DowngradeCtx<'_> {
self.irs
.get(id.0 - self.ctx.irs.len())
.expect("ExprId out of bounds")
.as_ref()
.expect("maybe_thunk called on an extracted expr")
}
}
@@ -404,22 +402,9 @@ impl DowngradeContext for DowngradeCtx<'_> {
})
}
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);
*self.irs.get_mut(local_id).expect("ExprId out of bounds") = expr;
}
fn get_current_source(&self) -> Source {
@@ -429,8 +414,11 @@ impl DowngradeContext for DowngradeCtx<'_> {
#[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)
let range = (start..start + slots).map(ExprId);
let span = synthetic_span();
// Fill reserved slots with placeholder value
self.irs.extend(range.clone().map(|slot| Thunk { inner: slot, span }.to_ir()));
range
}
fn downgrade(mut self, root: rnix::ast::Expr) -> Result<ExprId> {
@@ -441,7 +429,7 @@ impl DowngradeContext for DowngradeCtx<'_> {
let top_level = self.new_expr(TopLevel { body, thunks, span }.to_ir());
self.ctx
.irs
.extend(self.irs.into_iter().map(Option::unwrap));
.extend(self.irs);
Ok(top_level)
}

View File

@@ -3,8 +3,7 @@ use hashbrown::HashMap;
use rnix::{TextRange, ast};
use string_interner::symbol::SymbolU32;
use crate::error::{Error, Result, Source};
use crate::value::format_symbol;
use crate::error::{Result, Source};
use nix_js_macros::ir;
mod downgrade;
@@ -28,7 +27,6 @@ pub trait DowngradeContext {
fn lookup(&mut self, sym: SymId, span: TextRange) -> Result<ExprId>;
fn get_ir(&self, id: ExprId) -> &Ir;
fn extract_ir(&mut self, id: ExprId) -> Ir;
fn replace_ir(&mut self, id: ExprId, expr: Ir);
fn reserve_slots(&mut self, slots: usize) -> impl Iterator<Item = ExprId> + Clone + use<Self>;
fn get_current_source(&self) -> Source;
@@ -77,134 +75,6 @@ ir! {
CurPos,
}
impl Ir {
pub fn span(&self) -> TextRange {
match self {
Ir::Int(i) => i.span,
Ir::Float(f) => f.span,
Ir::Bool(b) => b.span,
Ir::Null(n) => n.span,
Ir::Str(s) => s.span,
Ir::AttrSet(a) => a.span,
Ir::List(l) => l.span,
Ir::HasAttr(h) => h.span,
Ir::BinOp(b) => b.span,
Ir::UnOp(u) => u.span,
Ir::Select(s) => s.span,
Ir::If(i) => i.span,
Ir::Call(c) => c.span,
Ir::Assert(a) => a.span,
Ir::ConcatStrings(c) => c.span,
Ir::Path(p) => p.span,
Ir::Func(f) => f.span,
Ir::TopLevel(t) => t.span,
Ir::Arg(a) => a.span,
Ir::Thunk(e) => e.span,
Ir::Builtins(b) => b.span,
Ir::Builtin(b) => b.span,
Ir::CurPos(c) => c.span,
}
}
}
impl AttrSet {
fn _insert(
&mut self,
mut path: impl Iterator<Item = Attr>,
name: Attr,
value: ExprId,
ctx: &mut impl DowngradeContext,
) -> Result<()> {
if let Some(attr) = path.next() {
// If the path is not yet exhausted, we need to recurse deeper.
match attr {
Attr::Str(ident, span) => {
// If the next attribute is a static string.
if let Some(&(id, _)) = self.stcs.get(&ident) {
// If a sub-attrset already exists, recurse into it.
let mut ir = ctx.extract_ir(id);
let result = ir
.as_mut()
.try_unwrap_attr_set()
.map_err(|_| {
// This path segment exists but is not an attrset, which is an error.
Error::downgrade_error(format!(
"attribute '{}' already defined but is not an attribute set",
format_symbol(ctx.get_sym(ident)),
),
ctx.get_current_source(),
span
)
})
.and_then(|attrs| attrs._insert(path, name, value, ctx));
ctx.replace_ir(id, ir);
result?;
} else {
// Create a new sub-attrset because this path doesn't exist yet.
// FIXME: span
let mut attrs = AttrSet {
stcs: Default::default(),
dyns: Default::default(),
span,
};
attrs._insert(path, name, value, ctx)?;
let attrs_expr = ctx.new_expr(attrs.to_ir());
self.stcs.insert(ident, (attrs_expr, span));
}
Ok(())
}
Attr::Dynamic(dynamic, span) => {
// If the next attribute is a dynamic expression, we must create a new sub-attrset.
// We cannot merge with existing dynamic attributes at this stage.
// FIXME: span
let mut attrs = AttrSet {
stcs: Default::default(),
dyns: Default::default(),
span,
};
attrs._insert(path, name, value, ctx)?;
self.dyns.push((dynamic, ctx.new_expr(attrs.to_ir()), span));
Ok(())
}
}
} else {
// This is the final attribute in the path, so insert the value here.
match name {
Attr::Str(ident, span) => {
if self.stcs.insert(ident, (value, span)).is_some() {
return Err(Error::downgrade_error(
format!(
"attribute '{}' already defined",
format_symbol(ctx.get_sym(ident)),
),
ctx.get_current_source(),
span,
));
}
}
Attr::Dynamic(dynamic, span) => {
self.dyns.push((dynamic, value, span));
}
}
Ok(())
}
}
fn insert(
&mut self,
path: Vec<Attr>,
value: ExprId,
ctx: &mut impl DowngradeContext,
) -> Result<()> {
let mut path = path.into_iter();
// The last part of the path is the name of the attribute to be inserted.
let name = path
.next_back()
.expect("empty attrpath passed. this is a bug");
self._insert(path, name, value, ctx)
}
}
#[repr(transparent)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ExprId(pub usize);
@@ -216,6 +86,7 @@ pub type SymId = SymbolU32;
pub struct ArgId(pub usize);
/// Represents a key in an attribute path.
#[allow(unused)]
#[derive(Debug, TryUnwrap)]
pub enum Attr {
/// A dynamic attribute key, which is an expression that must evaluate to a string.

View File

@@ -220,21 +220,7 @@ impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::AttrSet {
// rec { a = 1; b = a; } => let a = 1; b = a; in { inherit a b; }
let entries: Vec<_> = self.entries().collect();
downgrade_let_bindings(entries, ctx, span, |ctx, binding_keys| {
// Create plain attrset as body with inherit
let mut attrs = AttrSet {
stcs: HashMap::new(),
dyns: Vec::new(),
span,
};
for sym in binding_keys {
let expr = ctx.lookup(*sym, synthetic_span())?;
attrs.stcs.insert(*sym, (expr, synthetic_span()));
}
Ok(ctx.new_expr(attrs.to_ir()))
})
downgrade_rec_bindings(entries, ctx, span)
}
}
@@ -321,10 +307,9 @@ impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::Select {
impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::LegacyLet {
fn downgrade(self, ctx: &mut Ctx) -> Result<ExprId> {
let span = self.syntax().text_range();
let bindings = downgrade_static_attrs(self, ctx)?;
let binding_keys: Vec<_> = bindings.keys().copied().collect();
let attrset_expr = ctx.with_let_scope(bindings, |ctx| {
let entries: Vec<_> = self.entries().collect();
let attrset_expr = downgrade_let_bindings(entries, ctx, span, |ctx, binding_keys| {
// Create plain attrset as body with inherit
let mut attrs = AttrSet {
stcs: HashMap::new(),
dyns: Vec::new(),
@@ -332,12 +317,11 @@ impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::LegacyLet {
};
for sym in binding_keys {
// FIXME: span
let expr = ctx.lookup(sym, synthetic_span())?;
attrs.stcs.insert(sym, (expr, synthetic_span()));
let expr = ctx.lookup(*sym, synthetic_span())?;
attrs.stcs.insert(*sym, (expr, synthetic_span()));
}
Result::Ok(ctx.new_expr(attrs.to_ir()))
Ok(ctx.new_expr(attrs.to_ir()))
})?;
let body_sym = ctx.new_sym("body".to_string());

View File

@@ -5,7 +5,7 @@ use hashbrown::hash_map::Entry;
use hashbrown::{HashMap, HashSet};
use itertools::Itertools as _;
use rnix::TextRange;
use rnix::ast;
use rnix::ast::{self, HasEntry};
use rowan::ast::AstNode;
use crate::error::{Error, Result};
@@ -14,75 +14,303 @@ use crate::value::format_symbol;
use super::*;
/// Downgrades the entries of an attribute set.
/// This handles `inherit` and `attrpath = value;` entries.
pub fn downgrade_attrs(
attrs: impl ast::HasEntry + AstNode,
ctx: &mut impl DowngradeContext,
) -> Result<AttrSet> {
let entries = attrs.entries();
let mut attrs = AttrSet {
stcs: HashMap::new(),
dyns: Vec::new(),
span: attrs.syntax().text_range(),
};
for entry in entries {
match entry {
ast::Entry::Inherit(inherit) => downgrade_inherit(inherit, &mut attrs.stcs, ctx)?,
ast::Entry::AttrpathValue(value) => downgrade_attrpathvalue(value, &mut attrs, ctx)?,
}
}
Ok(attrs)
enum PendingValue {
Expr(ast::Expr),
InheritFrom(ast::Expr, SymId, TextRange),
InheritScope(SymId, TextRange),
Set(PendingAttrSet),
ExtendedRecAttrSet {
base: ast::AttrSet,
extensions: Vec<ast::Entry>,
span: TextRange,
},
}
/// Downgrades attribute set entries for a `let...in` expression.
/// This is a stricter version of `downgrade_attrs` that disallows dynamic attributes,
/// as `let` bindings must be statically known.
pub fn downgrade_static_attrs(
attrs: impl ast::HasEntry + AstNode,
ctx: &mut impl DowngradeContext,
) -> Result<HashMap<SymId, ExprId>> {
let entries = attrs.entries();
let mut attrs = AttrSet {
stcs: HashMap::new(),
dyns: Vec::new(),
span: attrs.syntax().text_range(),
};
for entry in entries {
match entry {
ast::Entry::Inherit(inherit) => downgrade_inherit(inherit, &mut attrs.stcs, ctx)?,
ast::Entry::AttrpathValue(value) => {
downgrade_static_attrpathvalue(value, &mut attrs, ctx)?
}
}
}
Ok(attrs.stcs.into_iter().map(|(k, (v, _))| (k, v)).collect())
struct PendingAttrSet {
stcs: HashMap<SymId, (PendingValue, TextRange)>,
dyns: Vec<(ast::Attr, PendingValue, TextRange)>,
span: TextRange,
}
/// Downgrades an `inherit` statement.
/// `inherit (from) a b;` is translated into `a = from.a; b = from.b;`.
/// `inherit a b;` is translated into `a = a; b = b;` (i.e., bringing variables into scope).
pub fn downgrade_inherit(
inherit: ast::Inherit,
stcs: &mut HashMap<SymId, (ExprId, rnix::TextRange)>,
impl PendingAttrSet {
fn new(span: TextRange) -> Self {
Self {
stcs: HashMap::new(),
dyns: Vec::new(),
span,
}
}
fn insert(
&mut self,
path: &[ast::Attr],
value: ast::Expr,
ctx: &mut impl DowngradeContext,
) -> Result<()> {
// Downgrade the `from` expression if it exists.
let from = if let Some(from) = inherit.from() {
Some(from.expr().unwrap().downgrade(ctx)?)
) -> Result<()> {
let first = path.first().expect("empty attrpath passed. this is a bug");
let rest = &path[1..];
let span = first.syntax().text_range();
match first {
ast::Attr::Ident(ident) => {
let sym = ctx.new_sym(ident.to_string());
self.insert_static(sym, span, rest, value, ctx)
}
ast::Attr::Str(string) => {
let parts = string.normalized_parts();
if parts.len() == 1
&& let ast::InterpolPart::Literal(lit) = parts.into_iter().next().unwrap()
{
let sym = ctx.new_sym(lit);
return self.insert_static(sym, span, rest, value, ctx);
}
self.insert_dynamic(first.clone(), span, rest, value)
}
ast::Attr::Dynamic(_) => self.insert_dynamic(first.clone(), span, rest, value),
}
}
fn insert_static(
&mut self,
sym: SymId,
span: TextRange,
path: &[ast::Attr],
value: ast::Expr,
ctx: &mut impl DowngradeContext,
) -> Result<()> {
if !path.is_empty() {
match self.stcs.entry(sym) {
Entry::Occupied(mut entry) => {
let (existing, existing_span) = entry.get_mut();
if let PendingValue::Expr(ast::Expr::AttrSet(attrset)) = existing
&& attrset.rec_token().is_some()
{
let base = attrset.clone();
let base_span = attrset.syntax().text_range();
let ext_entry = make_attrpath_value_entry(path.to_vec(), value);
*existing = PendingValue::ExtendedRecAttrSet {
base,
extensions: vec![ext_entry],
span: base_span,
};
return Ok(());
}
if let PendingValue::ExtendedRecAttrSet { extensions, .. } = existing {
let ext_entry = make_attrpath_value_entry(path.to_vec(), value);
extensions.push(ext_entry);
return Ok(());
}
let nested = Self::ensure_pending_set(existing, ctx, *existing_span)?;
nested.insert(path, value, ctx)
}
Entry::Vacant(entry) => {
let mut nested = PendingAttrSet::new(span);
nested.insert(path, value, ctx)?;
entry.insert((PendingValue::Set(nested), span));
Ok(())
}
}
} else {
None
match self.stcs.entry(sym) {
Entry::Occupied(mut entry) => {
let (existing, existing_span) = entry.get_mut();
Self::merge_value(existing, *existing_span, value, span, ctx)
}
Entry::Vacant(entry) => {
entry.insert((PendingValue::Expr(value), span));
Ok(())
}
}
}
}
fn insert_dynamic(
&mut self,
attr: ast::Attr,
span: TextRange,
path: &[ast::Attr],
value: ast::Expr,
) -> Result<()> {
if !path.is_empty() {
let mut nested = PendingAttrSet::new(span);
nested.insert_dynamic(
path[0].clone(),
path[0].syntax().text_range(),
&path[1..],
value,
)?;
self.dyns.push((attr, PendingValue::Set(nested), span));
} else {
self.dyns.push((attr, PendingValue::Expr(value), span));
}
Ok(())
}
fn ensure_pending_set<'a>(
value: &'a mut PendingValue,
ctx: &mut impl DowngradeContext,
span: TextRange,
) -> Result<&'a mut PendingAttrSet> {
match value {
PendingValue::Set(set) => Ok(set),
PendingValue::Expr(expr) => {
if let ast::Expr::AttrSet(attrset) = expr {
let mut nested = PendingAttrSet::new(attrset.syntax().text_range());
nested.collect_entries(attrset.entries(), ctx)?;
*value = PendingValue::Set(nested);
match value {
PendingValue::Set(set) => Ok(set),
_ => unreachable!(),
}
} else {
Err(Error::downgrade_error(
"attribute already defined but is not an attribute set".to_string(),
ctx.get_current_source(),
span,
))
}
}
PendingValue::ExtendedRecAttrSet { .. } => Err(Error::downgrade_error(
"cannot add nested attributes to rec attribute set with extensions".to_string(),
ctx.get_current_source(),
span,
)),
PendingValue::InheritFrom(..) | PendingValue::InheritScope(..) => {
Err(Error::downgrade_error(
"attribute already defined (inherited)".to_string(),
ctx.get_current_source(),
span,
))
}
}
}
fn merge_value(
existing: &mut PendingValue,
existing_span: TextRange,
new_value: ast::Expr,
new_span: TextRange,
ctx: &mut impl DowngradeContext,
) -> Result<()> {
match existing {
PendingValue::Set(existing_set) => {
if let ast::Expr::AttrSet(new_attrset) = new_value {
existing_set.collect_entries(new_attrset.entries(), ctx)
} else {
Err(Error::downgrade_error(
"attribute already defined as attribute set".to_string(),
ctx.get_current_source(),
new_span,
))
}
}
PendingValue::Expr(existing_expr) => {
if let ast::Expr::AttrSet(existing_attrset) = existing_expr {
if let ast::Expr::AttrSet(new_attrset) = new_value {
let is_rec = existing_attrset.rec_token().is_some();
if is_rec {
let base = existing_attrset.clone();
let extensions: Vec<_> = new_attrset.entries().collect();
*existing = PendingValue::ExtendedRecAttrSet {
base,
extensions,
span: existing_attrset.syntax().text_range(),
};
Ok(())
} else {
let mut merged =
PendingAttrSet::new(existing_attrset.syntax().text_range());
merged.collect_entries(existing_attrset.entries(), ctx)?;
merged.collect_entries(new_attrset.entries(), ctx)?;
*existing = PendingValue::Set(merged);
Ok(())
}
} else {
Err(Error::downgrade_error(
"attribute already defined as attribute set".to_string(),
ctx.get_current_source(),
new_span,
))
}
} else {
Err(Error::downgrade_error(
"attribute already defined".to_string(),
ctx.get_current_source(),
existing_span,
))
}
}
PendingValue::ExtendedRecAttrSet { extensions, .. } => {
if let ast::Expr::AttrSet(new_attrset) = new_value {
extensions.extend(new_attrset.entries());
Ok(())
} else {
Err(Error::downgrade_error(
"attribute already defined as attribute set".to_string(),
ctx.get_current_source(),
new_span,
))
}
}
PendingValue::InheritFrom(..) | PendingValue::InheritScope(..) => {
Err(Error::downgrade_error(
"attribute already defined (inherited)".to_string(),
ctx.get_current_source(),
existing_span,
))
}
}
}
fn collect_entries(
&mut self,
entries: impl Iterator<Item = ast::Entry>,
ctx: &mut impl DowngradeContext,
) -> Result<()> {
for entry in entries {
match entry {
ast::Entry::Inherit(inherit) => {
self.collect_inherit(inherit, ctx)?;
}
ast::Entry::AttrpathValue(value) => {
let attrpath = value.attrpath().unwrap();
let path: Vec<_> = attrpath.attrs().collect();
let expr = value.value().unwrap();
self.insert(&path, expr, ctx)?;
}
}
}
Ok(())
}
fn collect_inherit(
&mut self,
inherit: ast::Inherit,
ctx: &mut impl DowngradeContext,
) -> Result<()> {
let from = inherit.from().map(|f| f.expr().unwrap());
for attr in inherit.attrs() {
let span = attr.syntax().text_range();
let ident = match downgrade_attr(attr, ctx)? {
Attr::Str(ident, _) => ident,
_ => {
// `inherit` does not allow dynamic attributes.
let sym = match &attr {
ast::Attr::Ident(ident) => ctx.new_sym(ident.to_string()),
ast::Attr::Str(s) => {
let parts = s.normalized_parts();
if parts.len() == 1
&& let ast::InterpolPart::Literal(lit) = parts.into_iter().next().unwrap()
{
ctx.new_sym(lit)
} else {
return Err(Error::downgrade_error(
"dynamic attributes not allowed in inherit".to_string(),
ctx.get_current_source(),
span,
));
}
}
ast::Attr::Dynamic(_) => {
return Err(Error::downgrade_error(
"dynamic attributes not allowed in inherit".to_string(),
ctx.get_current_source(),
@@ -90,41 +318,100 @@ pub fn downgrade_inherit(
));
}
};
let expr = if let Some(expr) = from {
let select_expr = ctx.new_expr(
Select {
expr,
attrpath: vec![Attr::Str(ident, span)],
default: None,
span,
}
.to_ir(),
);
ctx.maybe_thunk(select_expr)
} else {
ctx.lookup(ident, span)?
};
match stcs.entry(ident) {
Entry::Occupied(occupied) => {
if self.stcs.contains_key(&sym) {
return Err(Error::downgrade_error(
format!(
"attribute '{}' already defined",
format_symbol(ctx.get_sym(*occupied.key()))
format_symbol(ctx.get_sym(sym))
),
ctx.get_current_source(),
span,
)
.with_span(span)
.with_source(ctx.get_current_source()));
));
}
Entry::Vacant(vacant) => vacant.insert((expr, span)),
let pending_value = if let Some(ref from_expr) = from {
PendingValue::InheritFrom(from_expr.clone(), sym, span)
} else {
PendingValue::InheritScope(sym, span)
};
self.stcs.insert(sym, (pending_value, span));
}
Ok(())
}
}
fn make_attrpath_value_entry(path: Vec<ast::Attr>, value: ast::Expr) -> ast::Entry {
use rnix::{NixLanguage, SyntaxKind};
use rowan::{GreenNodeBuilder, Language, NodeOrToken};
let mut builder = GreenNodeBuilder::new();
builder.start_node(NixLanguage::kind_to_raw(SyntaxKind::NODE_ATTRPATH_VALUE));
builder.start_node(NixLanguage::kind_to_raw(SyntaxKind::NODE_ATTRPATH));
for attr in path {
fn add_node(builder: &mut GreenNodeBuilder, node: &rowan::SyntaxNode<NixLanguage>) {
for child in node.children_with_tokens() {
match child {
NodeOrToken::Node(n) => {
builder.start_node(NixLanguage::kind_to_raw(n.kind()));
add_node(builder, &n);
builder.finish_node();
}
NodeOrToken::Token(t) => {
builder.token(NixLanguage::kind_to_raw(t.kind()), t.text());
}
}
}
}
builder.start_node(NixLanguage::kind_to_raw(attr.syntax().kind()));
add_node(&mut builder, attr.syntax());
builder.finish_node();
}
builder.finish_node();
builder.token(NixLanguage::kind_to_raw(SyntaxKind::TOKEN_ASSIGN), "=");
builder.start_node(NixLanguage::kind_to_raw(value.syntax().kind()));
fn add_node_value(builder: &mut GreenNodeBuilder, node: &rowan::SyntaxNode<NixLanguage>) {
for child in node.children_with_tokens() {
match child {
NodeOrToken::Node(n) => {
builder.start_node(NixLanguage::kind_to_raw(n.kind()));
add_node_value(builder, &n);
builder.finish_node();
}
NodeOrToken::Token(t) => {
builder.token(NixLanguage::kind_to_raw(t.kind()), t.text());
}
}
}
}
add_node_value(&mut builder, value.syntax());
builder.finish_node();
builder.token(NixLanguage::kind_to_raw(SyntaxKind::TOKEN_SEMICOLON), ";");
builder.finish_node();
let green = builder.finish();
let node = rowan::SyntaxNode::<NixLanguage>::new_root(green);
ast::Entry::cast(node).unwrap()
}
/// Downgrades the entries of a non-recursive attribute set.
pub fn downgrade_attrs(
attrs: impl ast::HasEntry + AstNode,
ctx: &mut impl DowngradeContext,
) -> Result<AttrSet> {
let span = attrs.syntax().text_range();
let mut pending = PendingAttrSet::new(span);
pending.collect_entries(attrs.entries(), ctx)?;
finalize_pending_set::<_, true>(pending, &HashMap::new(), ctx)
}
/// Downgrades a single attribute key (part of an attribute path).
/// An attribute can be a static identifier, an interpolated string, or a dynamic expression.
pub fn downgrade_attr(attr: ast::Attr, ctx: &mut impl DowngradeContext) -> Result<Attr> {
use ast::Attr::*;
use ast::InterpolPart::*;
@@ -139,7 +426,6 @@ pub fn downgrade_attr(attr: ast::Attr, ctx: &mut impl DowngradeContext) -> Resul
if parts.is_empty() {
Ok(Attr::Str(ctx.new_sym("".to_string()), span))
} else if parts.len() == 1 {
// If the string has only one part, it's either a literal or a single interpolation.
match parts.into_iter().next().unwrap() {
Literal(ident) => Ok(Attr::Str(ctx.new_sym(ident), span)),
Interpolation(interpol) => Ok(Attr::Dynamic(
@@ -148,7 +434,6 @@ pub fn downgrade_attr(attr: ast::Attr, ctx: &mut impl DowngradeContext) -> Resul
)),
}
} else {
// If the string has multiple parts, it's an interpolated string that must be concatenated.
let parts = parts
.into_iter()
.map(|part| match part {
@@ -180,40 +465,6 @@ pub fn downgrade_attrpath(
.collect::<Result<Vec<_>>>()
}
/// Downgrades an `attrpath = value;` expression and inserts it into an `AttrSet`.
pub fn downgrade_attrpathvalue(
value: ast::AttrpathValue,
attrs: &mut AttrSet,
ctx: &mut impl DowngradeContext,
) -> Result<()> {
let path = downgrade_attrpath(value.attrpath().unwrap(), ctx)?;
let value = value.value().unwrap().downgrade(ctx)?;
let value = ctx.maybe_thunk(value);
attrs.insert(path, value, ctx)
}
/// A stricter version of `downgrade_attrpathvalue` for `let...in` bindings.
/// It ensures that the attribute path contains no dynamic parts.
pub fn downgrade_static_attrpathvalue(
value: ast::AttrpathValue,
attrs: &mut AttrSet,
ctx: &mut impl DowngradeContext,
) -> Result<()> {
let attrpath_node = value.attrpath().unwrap();
let path = downgrade_attrpath(attrpath_node.clone(), ctx)?;
if let Some(&Attr::Dynamic(_, span)) =
path.iter().find(|attr| matches!(attr, Attr::Dynamic(..)))
{
return Err(Error::downgrade_error(
"dynamic attributes not allowed in let bindings".to_string(),
ctx.get_current_source(),
span,
));
}
let value = value.value().unwrap().downgrade(ctx)?;
attrs.insert(path, value, ctx)
}
pub struct PatternBindings {
pub body: ExprId,
pub required: Vec<SymId>,
@@ -221,7 +472,6 @@ pub struct PatternBindings {
}
/// Helper function for Lambda pattern parameters.
/// Processes pattern entries like `{ a, b ? 2, ... }@alias` and creates bindings.
pub fn downgrade_pattern_bindings<Ctx>(
pat_entries: impl Iterator<Item = ast::PatEntry>,
alias: Option<SymId>,
@@ -337,67 +587,67 @@ where
})
}
/// Helper function to downgrade entries with let bindings semantics.
/// This extracts common logic for both `rec` attribute sets and `let...in` expressions.
/// Downgrades a `let...in` expression. This is a special case of rec attrs
/// that disallows dynamic attributes and has a body expression.
pub fn downgrade_let_bindings<Ctx, F>(
entries: Vec<ast::Entry>,
ctx: &mut Ctx,
_span: TextRange,
span: TextRange,
body_fn: F,
) -> Result<ExprId>
where
Ctx: DowngradeContext,
F: FnOnce(&mut Ctx, &[SymId]) -> Result<ExprId>,
{
let mut binding_syms = HashSet::new();
downgrade_rec_attrs_impl::<_, _, false>(entries, ctx, span, |ctx, binding_keys, _dyns| {
body_fn(ctx, binding_keys)
})
}
for entry in &entries {
match entry {
ast::Entry::Inherit(inherit) => {
for attr in inherit.attrs() {
if let ast::Attr::Ident(ident) = attr {
let sym = ctx.new_sym(ident.to_string());
if !binding_syms.insert(sym) {
return Err(Error::downgrade_error(
format!(
"attribute '{}' already defined",
format_symbol(ctx.get_sym(sym))
),
ctx.get_current_source(),
ident.syntax().text_range(),
));
}
}
}
}
ast::Entry::AttrpathValue(value) => {
let attrpath = value.attrpath().unwrap();
let attrs_vec: Vec<_> = attrpath.attrs().collect();
/// Downgrades a `rec` attribute set.
pub fn downgrade_rec_bindings<Ctx>(
entries: Vec<ast::Entry>,
ctx: &mut Ctx,
span: TextRange,
) -> Result<ExprId>
where
Ctx: DowngradeContext,
{
downgrade_rec_attrs_impl::<_, _, true>(entries, ctx, span, |ctx, binding_keys, dyns| {
let mut attrs = AttrSet {
stcs: HashMap::new(),
dyns: dyns.to_vec(),
span,
};
if attrs_vec.len() == 1 {
if let Some(ast::Attr::Ident(ident)) = attrs_vec.first() {
let sym = ctx.new_sym(ident.to_string());
if !binding_syms.insert(sym) {
return Err(Error::downgrade_error(
format!(
"attribute '{}' already defined",
format_symbol(ctx.get_sym(sym))
),
ctx.get_current_source(),
ident.syntax().text_range(),
));
}
}
} else if attrs_vec.len() > 1
&& let Some(ast::Attr::Ident(ident)) = attrs_vec.first()
{
let sym = ctx.new_sym(ident.to_string());
binding_syms.insert(sym);
}
}
}
for sym in binding_keys {
let expr = ctx.lookup(*sym, synthetic_span())?;
attrs.stcs.insert(*sym, (expr, synthetic_span()));
}
Ok(ctx.new_expr(attrs.to_ir()))
})
}
/// Core implementation for recursive bindings (rec attrs and let-in).
/// ALLOW_DYN controls whether dynamic attributes are allowed.
fn downgrade_rec_attrs_impl<Ctx, F, const ALLOW_DYN: bool>(
entries: Vec<ast::Entry>,
ctx: &mut Ctx,
span: TextRange,
body_fn: F,
) -> Result<ExprId>
where
Ctx: DowngradeContext,
F: FnOnce(&mut Ctx, &[SymId], &[(ExprId, ExprId, TextRange)]) -> Result<ExprId>,
{
let mut pending = PendingAttrSet::new(span);
pending.collect_entries(entries.iter().cloned(), ctx)?;
let binding_syms = collect_binding_syms::<_, ALLOW_DYN>(&pending, ctx)?;
let inherit_lookups = collect_inherit_lookups(&entries, ctx)?;
let binding_keys: Vec<_> = binding_syms.into_iter().collect();
let slots: Vec<_> = ctx.reserve_slots(binding_keys.len()).collect();
let let_bindings: HashMap<_, _> = binding_keys
@@ -407,30 +657,22 @@ where
.collect();
for &slot in &slots {
let span = synthetic_span();
ctx.replace_ir(slot, Thunk { inner: slot, span }.to_ir());
let slot_span = synthetic_span();
ctx.replace_ir(
slot,
Thunk {
inner: slot,
span: slot_span,
}
.to_ir(),
);
}
ctx.with_let_scope(let_bindings.clone(), |ctx| {
let mut temp_attrs = AttrSet {
stcs: HashMap::new(),
dyns: Vec::new(),
span: synthetic_span(),
};
for entry in entries {
match entry {
ast::Entry::Inherit(inherit) => {
downgrade_inherit(inherit, &mut temp_attrs.stcs, ctx)?;
}
ast::Entry::AttrpathValue(value) => {
downgrade_static_attrpathvalue(value, &mut temp_attrs, ctx)?;
}
}
}
let finalized = finalize_pending_set::<_, ALLOW_DYN>(pending, &inherit_lookups, ctx)?;
for (sym, slot) in binding_keys.iter().copied().zip(slots.iter()) {
if let Some(&(expr, _)) = temp_attrs.stcs.get(&sym) {
if let Some(&(expr, _)) = finalized.stcs.get(&sym) {
ctx.register_thunk(*slot, expr);
} else {
return Err(Error::internal(format!(
@@ -440,6 +682,163 @@ where
}
}
body_fn(ctx, &binding_keys)
body_fn(ctx, &binding_keys, &finalized.dyns)
})
}
/// Collects `inherit x` lookups from the outer scope before entering the rec scope.
fn collect_inherit_lookups<Ctx: DowngradeContext>(
entries: &[ast::Entry],
ctx: &mut Ctx,
) -> Result<HashMap<SymId, (ExprId, TextRange)>> {
let mut inherit_lookups = HashMap::new();
for entry in entries {
if let ast::Entry::Inherit(inherit) = entry
&& inherit.from().is_none()
{
for attr in inherit.attrs() {
if let ast::Attr::Ident(ident) = attr {
let attr_span = ident.syntax().text_range();
let sym = ctx.new_sym(ident.to_string());
let expr = ctx.lookup(sym, attr_span)?;
inherit_lookups.insert(sym, (expr, attr_span));
}
}
}
}
Ok(inherit_lookups)
}
/// Collects binding symbols from a pending set, checking for duplicates.
fn collect_binding_syms<Ctx: DowngradeContext, const ALLOW_DYN: bool>(
pending: &PendingAttrSet,
ctx: &mut Ctx,
) -> Result<HashSet<SymId>> {
let mut binding_syms = HashSet::new();
for (sym, (_, span)) in &pending.stcs {
if !binding_syms.insert(*sym) {
return Err(Error::downgrade_error(
format!(
"attribute '{}' already defined",
format_symbol(ctx.get_sym(*sym))
),
ctx.get_current_source(),
*span,
));
}
}
Ok(binding_syms)
}
/// Unified finalize function for PendingAttrSet.
/// ALLOW_DYN controls whether dynamic attributes are allowed.
fn finalize_pending_set<Ctx: DowngradeContext, const ALLOW_DYN: bool>(
pending: PendingAttrSet,
inherit_lookups: &HashMap<SymId, (ExprId, TextRange)>,
ctx: &mut Ctx,
) -> Result<AttrSet> {
let mut stcs = HashMap::new();
let mut dyns = Vec::new();
for (sym, (value, value_span)) in pending.stcs {
let expr_id = finalize_pending_value::<_, ALLOW_DYN>(value, inherit_lookups, ctx)?;
stcs.insert(sym, (expr_id, value_span));
}
if ALLOW_DYN {
for (attr, value, value_span) in pending.dyns {
let key_id = downgrade_attr(attr, ctx)?;
let key_expr = match key_id {
Attr::Dynamic(id, _) => id,
Attr::Str(sym, attr_span) => ctx.new_expr(
Str {
val: ctx.get_sym(sym).to_string(),
span: attr_span,
}
.to_ir(),
),
};
let value_id = finalize_pending_value::<_, ALLOW_DYN>(value, inherit_lookups, ctx)?;
dyns.push((key_expr, value_id, value_span));
}
}
Ok(AttrSet {
stcs,
dyns,
span: pending.span,
})
}
/// Unified finalize function for PendingValue.
/// ALLOW_DYN controls whether dynamic attributes are allowed.
fn finalize_pending_value<Ctx: DowngradeContext, const ALLOW_DYN: bool>(
value: PendingValue,
inherit_lookups: &HashMap<SymId, (ExprId, TextRange)>,
ctx: &mut Ctx,
) -> Result<ExprId> {
match value {
PendingValue::Expr(expr) => {
if !ALLOW_DYN {
check_no_dynamic_attrs(&expr, ctx)?;
}
let id = Downgrade::downgrade(expr, ctx)?;
Ok(ctx.maybe_thunk(id))
}
PendingValue::InheritFrom(from_expr, sym, span) => {
let from_id = Downgrade::downgrade(from_expr, ctx)?;
let select = Select {
expr: from_id,
attrpath: vec![Attr::Str(sym, span)],
default: None,
span,
};
let select_id = ctx.new_expr(select.to_ir());
Ok(ctx.maybe_thunk(select_id))
}
PendingValue::InheritScope(sym, span) => {
if let Some(&(expr, _)) = inherit_lookups.get(&sym) {
Ok(ctx.maybe_thunk(expr))
} else {
let lookup_id = ctx.lookup(sym, span)?;
Ok(ctx.maybe_thunk(lookup_id))
}
}
PendingValue::Set(set) => {
let attrset = finalize_pending_set::<_, ALLOW_DYN>(set, inherit_lookups, ctx)?;
Ok(ctx.new_expr(attrset.to_ir()))
}
PendingValue::ExtendedRecAttrSet {
base,
extensions,
span,
} => {
let mut all_entries: Vec<_> = base.entries().collect();
all_entries.extend(extensions);
downgrade_rec_bindings(all_entries, ctx, span)
}
}
}
/// Check that an expression doesn't contain dynamic attributes (for let bindings).
fn check_no_dynamic_attrs(expr: &ast::Expr, ctx: &impl DowngradeContext) -> Result<()> {
let ast::Expr::AttrSet(attrset) = expr else {
return Ok(())
};
for v in attrset.attrpath_values() {
v.attrpath().unwrap().attrs().try_for_each(|attr| {
if let ast::Attr::Dynamic(dyn_attr) = attr {
Err(Error::downgrade_error(
"dynamic attributes not allowed in let bindings".to_string(),
ctx.get_current_source(),
dyn_attr.syntax().text_range(),
))
} else {
Ok(())
}
})?;
}
Ok(())
}

View File

@@ -552,6 +552,7 @@ pub(crate) struct Runtime<Ctx: RuntimeContext> {
primop_metadata_symbol: v8::Global<v8::Symbol>,
has_context_symbol: v8::Global<v8::Symbol>,
is_path_symbol: v8::Global<v8::Symbol>,
is_cycle_symbol: v8::Global<v8::Symbol>,
_marker: PhantomData<Ctx>,
}
@@ -571,7 +572,13 @@ impl<Ctx: RuntimeContext> Runtime<Ctx> {
..Default::default()
});
let (is_thunk_symbol, primop_metadata_symbol, has_context_symbol, is_path_symbol) = {
let (
is_thunk_symbol,
primop_metadata_symbol,
has_context_symbol,
is_path_symbol,
is_cycle_symbol,
) = {
deno_core::scope!(scope, &mut js_runtime);
Self::get_symbols(scope)?
};
@@ -582,6 +589,7 @@ impl<Ctx: RuntimeContext> Runtime<Ctx> {
primop_metadata_symbol,
has_context_symbol,
is_path_symbol,
is_cycle_symbol,
_marker: PhantomData,
})
}
@@ -609,6 +617,7 @@ impl<Ctx: RuntimeContext> Runtime<Ctx> {
let primop_metadata_symbol = v8::Local::new(scope, &self.primop_metadata_symbol);
let has_context_symbol = v8::Local::new(scope, &self.has_context_symbol);
let is_path_symbol = v8::Local::new(scope, &self.is_path_symbol);
let is_cycle_symbol = v8::Local::new(scope, &self.is_cycle_symbol);
Ok(to_value(
local_value,
@@ -617,10 +626,11 @@ impl<Ctx: RuntimeContext> Runtime<Ctx> {
primop_metadata_symbol,
has_context_symbol,
is_path_symbol,
is_cycle_symbol,
))
}
/// get (IS_THUNK, PRIMOP_METADATA, HAS_CONTEXT, IS_PATH)
/// get (IS_THUNK, PRIMOP_METADATA, HAS_CONTEXT, IS_PATH, IS_CYCLE)
#[allow(clippy::type_complexity)]
fn get_symbols(
scope: &ScopeRef,
@@ -629,6 +639,7 @@ impl<Ctx: RuntimeContext> Runtime<Ctx> {
v8::Global<v8::Symbol>,
v8::Global<v8::Symbol>,
v8::Global<v8::Symbol>,
v8::Global<v8::Symbol>,
)> {
let global = scope.get_current_context().global(scope);
let nix_key = v8::String::new(scope, "Nix")
@@ -659,8 +670,9 @@ impl<Ctx: RuntimeContext> Runtime<Ctx> {
let primop_metadata = get_symbol("PRIMOP_METADATA")?;
let has_context = get_symbol("HAS_CONTEXT")?;
let is_path = get_symbol("IS_PATH")?;
let is_cycle = get_symbol("IS_CYCLE")?;
Ok((is_thunk, primop_metadata, has_context, is_path))
Ok((is_thunk, primop_metadata, has_context, is_path, is_cycle))
}
}
@@ -671,6 +683,7 @@ fn to_value<'a>(
primop_metadata_symbol: LocalSymbol<'a>,
has_context_symbol: LocalSymbol<'a>,
is_path_symbol: LocalSymbol<'a>,
is_cycle_symbol: LocalSymbol<'a>,
) -> Value {
match () {
_ if val.is_big_int() => {
@@ -707,6 +720,7 @@ fn to_value<'a>(
primop_metadata_symbol,
has_context_symbol,
is_path_symbol,
is_cycle_symbol,
)
})
.collect();
@@ -724,6 +738,10 @@ fn to_value<'a>(
return Value::Thunk;
}
if is_cycle(val, scope, is_cycle_symbol) {
return Value::Repeated;
}
if let Some(path_val) = extract_path(val, scope, is_path_symbol) {
return Value::Path(path_val);
}
@@ -753,6 +771,7 @@ fn to_value<'a>(
primop_metadata_symbol,
has_context_symbol,
is_path_symbol,
is_cycle_symbol,
),
)
})
@@ -772,6 +791,15 @@ fn is_thunk<'a>(val: LocalValue<'a>, scope: &ScopeRef<'a, '_>, symbol: LocalSymb
matches!(obj.get(scope, symbol.into()), Some(v) if v.is_true())
}
fn is_cycle<'a>(val: LocalValue<'a>, scope: &ScopeRef<'a, '_>, symbol: LocalSymbol<'a>) -> bool {
if !val.is_object() {
return false;
}
let obj = val.to_object(scope).expect("infallible conversion");
matches!(obj.get(scope, symbol.into()), Some(v) if v.is_true())
}
fn extract_string_with_context<'a>(
val: LocalValue<'a>,
scope: &ScopeRef<'a, '_>,

View File

@@ -117,15 +117,9 @@ impl Debug for AttrSet {
impl Display for AttrSet {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
use Value::*;
write!(f, "{{")?;
for (k, v) in self.data.iter() {
write!(f, " {k} = ")?;
match v {
List(_) => write!(f, "[ ... ];")?,
AttrSet(_) => write!(f, "{{ ... }};")?,
v => write!(f, "{v};")?,
}
write!(f, " {k} = {v};")?;
}
write!(f, " }}")
}
@@ -199,7 +193,20 @@ impl Display for Value {
&Float(x) => write!(f, "{x}"),
&Bool(x) => write!(f, "{x}"),
Null => write!(f, "null"),
String(x) => write!(f, r#""{x}""#),
String(x) => {
write!(f, "\"")?;
for c in x.chars() {
match c {
'\\' => write!(f, "\\\\")?,
'"' => write!(f, "\\\"")?,
'\n' => write!(f, "\\n")?,
'\r' => write!(f, "\\r")?,
'\t' => write!(f, "\\t")?,
c => write!(f, "{c}")?,
}
}
write!(f, "\"")
}
Path(x) => write!(f, "{x}"),
AttrSet(x) => write!(f, "{x}"),
List(x) => write!(f, "{x}"),
@@ -207,7 +214,7 @@ impl Display for Value {
Func => write!(f, "«lambda»"),
PrimOp(name) => write!(f, "«primop {name}»"),
PrimOpApp(name) => write!(f, "«partially applied primop {name}»"),
Repeated => write!(f, "<REPEATED>"),
Repeated => write!(f, "«repeated»"),
}
}
}

View File

@@ -3,6 +3,8 @@ mod utils;
use nix_js::value::Value;
use utils::eval;
use crate::utils::eval_result;
#[test]
fn arithmetic() {
assert_eq!(eval("1 + 1"), Value::Int(2));
@@ -63,3 +65,8 @@ fn nested_let() {
Value::Int(3)
);
}
#[test]
fn rec_inherit_fails() {
assert!(eval_result("{ inherit x; }").is_err());
}

View File

@@ -470,8 +470,8 @@ fn structured_attrs_basic() {
Value::AttrSet(attrs) => {
assert!(attrs.contains_key("drvPath"));
assert!(attrs.contains_key("outPath"));
assert!(!attrs.contains_key("foo"));
assert!(!attrs.contains_key("count"));
assert!(attrs.contains_key("foo"));
assert!(attrs.contains_key("count"));
}
_ => panic!("Expected AttrSet"),
}
@@ -492,7 +492,7 @@ fn structured_attrs_nested() {
match result {
Value::AttrSet(attrs) => {
assert!(attrs.contains_key("drvPath"));
assert!(!attrs.contains_key("data"));
assert!(attrs.contains_key("data"));
}
_ => panic!("Expected AttrSet"),
}
@@ -554,7 +554,7 @@ fn ignore_nulls_true() {
match result {
Value::AttrSet(attrs) => {
assert!(attrs.contains_key("foo"));
assert!(!attrs.contains_key("nullValue"));
assert!(attrs.contains_key("nullValue"));
}
_ => panic!("Expected AttrSet"),
}
@@ -600,8 +600,8 @@ fn ignore_nulls_with_structured_attrs() {
match result {
Value::AttrSet(attrs) => {
assert!(attrs.contains_key("drvPath"));
assert!(!attrs.contains_key("foo"));
assert!(!attrs.contains_key("nullValue"));
assert!(attrs.contains_key("foo"));
assert!(attrs.contains_key("nullValue"));
}
_ => panic!("Expected AttrSet"),
}
@@ -627,8 +627,8 @@ fn all_features_combined() {
assert!(attrs.contains_key("out"));
assert!(attrs.contains_key("dev"));
assert!(attrs.contains_key("outPath"));
assert!(!attrs.contains_key("data"));
assert!(!attrs.contains_key("nullValue"));
assert!(attrs.contains_key("data"));
assert!(attrs.contains_key("nullValue"));
}
_ => panic!("Expected AttrSet"),
}
@@ -651,7 +651,7 @@ fn fixed_output_with_structured_attrs() {
Value::AttrSet(attrs) => {
assert!(attrs.contains_key("outPath"));
assert!(attrs.contains_key("drvPath"));
assert!(!attrs.contains_key("data"));
assert!(attrs.contains_key("data"));
}
_ => panic!("Expected AttrSet"),
}

278
nix-js/tests/lang.rs Normal file
View File

@@ -0,0 +1,278 @@
#![allow(non_snake_case)]
mod utils;
use std::path::PathBuf;
use nix_js::context::Context;
use nix_js::error::Source;
use nix_js::value::Value;
fn get_lang_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/lang")
}
fn eval_file(name: &str) -> Result<Value, String> {
let lang_dir = get_lang_dir();
let nix_path = lang_dir.join(format!("{name}.nix"));
let expr = format!(r#"import "{}""#, nix_path.display());
let mut ctx = Context::new().map_err(|e| e.to_string())?;
let source = Source::new_eval(expr).map_err(|e| e.to_string())?;
ctx.eval_code(source).map_err(|e| e.to_string())
}
fn read_expected(name: &str) -> String {
let lang_dir = get_lang_dir();
let exp_path = lang_dir.join(format!("{name}.exp"));
std::fs::read_to_string(exp_path)
.expect("expected file should exist")
.trim_end()
.to_string()
}
fn format_value(value: &Value) -> String {
value.to_string()
}
macro_rules! eval_okay_test {
($(#[$attr:meta])* $name:ident$(, $pre:expr)?) => {
$(#[$attr])*
#[test]
fn $name() {
$(($pre)();)?
let test_name = concat!("eval-okay-", stringify!($name))
.replace("_", "-")
.replace("r#", "");
let result = eval_file(&test_name);
match result {
Ok(value) => {
let actual = format_value(&value);
let expected = read_expected(&test_name);
assert_eq!(actual, expected, "Output mismatch for {}", test_name);
}
Err(e) => {
panic!("Test {} failed to evaluate: {}", test_name, e);
}
}
}
};
}
macro_rules! eval_fail_test {
($name:ident) => {
#[test]
fn $name() {
let test_name = concat!("eval-fail-", stringify!($name))
.replace("_", "-")
.replace("r#", "");
let result = eval_file(&test_name);
assert!(
result.is_err(),
"Test {} should have failed but succeeded with: {:?}",
test_name,
result
);
}
};
}
eval_okay_test!(any_all);
eval_okay_test!(arithmetic);
eval_okay_test!(attrnames);
eval_okay_test!(attrs);
eval_okay_test!(attrs2);
eval_okay_test!(attrs3);
eval_okay_test!(attrs4);
eval_okay_test!(attrs5);
eval_okay_test!(#[ignore = "__overrides is not supported"] attrs6);
eval_okay_test!(#[ignore = "requires --arg/--argstr CLI flags"] autoargs);
eval_okay_test!(backslash_newline_1);
eval_okay_test!(backslash_newline_2);
eval_okay_test!(baseNameOf);
eval_okay_test!(builtins);
eval_okay_test!(builtins_add);
eval_okay_test!(callable_attrs);
eval_okay_test!(catattrs);
eval_okay_test!(closure);
eval_okay_test!(comments);
eval_okay_test!(concat);
eval_okay_test!(concatmap);
eval_okay_test!(concatstringssep);
eval_okay_test!(context);
eval_okay_test!(context_introspection);
eval_okay_test!(#[ignore = "not implemented: convertHash"] convertHash);
eval_okay_test!(curpos);
eval_okay_test!(deepseq);
eval_okay_test!(delayed_with);
eval_okay_test!(delayed_with_inherit);
eval_okay_test!(deprecate_cursed_or);
eval_okay_test!(derivation_legacy);
eval_okay_test!(dynamic_attrs);
eval_okay_test!(dynamic_attrs_2);
eval_okay_test!(dynamic_attrs_bare);
eval_okay_test!(elem);
eval_okay_test!(empty_args);
eval_okay_test!(eq);
eval_okay_test!(eq_derivations);
eval_okay_test!(filter);
eval_okay_test!(#[ignore = "not implemented: flakeRefToString"] flake_ref_to_string);
eval_okay_test!(flatten);
eval_okay_test!(float);
eval_okay_test!(floor_ceil);
eval_okay_test!(foldlStrict);
eval_okay_test!(foldlStrict_lazy_elements);
eval_okay_test!(foldlStrict_lazy_initial_accumulator);
eval_okay_test!(fromjson);
eval_okay_test!(fromjson_escapes);
eval_okay_test!(#[ignore = "not implemented: fromTOML"] fromTOML);
eval_okay_test!(#[ignore = "not implemented: fromTOML"] fromTOML_timestamps);
eval_okay_test!(functionargs);
eval_okay_test!(#[ignore = "not implemented: hashFile"] hashfile);
eval_okay_test!(#[ignore = "not implemented: hashString"] hashstring);
eval_okay_test!(getattrpos);
eval_okay_test!(getattrpos_functionargs);
eval_okay_test!(getattrpos_undefined);
eval_okay_test!(getenv, || {
unsafe {
std::env::set_var("TEST_VAR", "foo")
};
});
eval_okay_test!(#[ignore = "not implemented: hashString"] groupBy);
eval_okay_test!(r#if);
eval_okay_test!(ind_string);
eval_okay_test!(#[ignore = "not implemented: scopedImport"] import);
eval_okay_test!(inherit_attr_pos);
eval_okay_test!(inherit_from);
eval_okay_test!(intersectAttrs);
eval_okay_test!(r#let);
eval_okay_test!(list);
eval_okay_test!(listtoattrs);
eval_okay_test!(logic);
eval_okay_test!(map);
eval_okay_test!(mapattrs);
eval_okay_test!(merge_dynamic_attrs);
eval_okay_test!(nested_with);
eval_okay_test!(new_let);
eval_okay_test!(null_dynamic_attrs);
eval_okay_test!(#[ignore = "__overrides is not supported"] overrides);
eval_okay_test!(#[ignore = "not implemented: parseFlakeRef"] parse_flake_ref);
eval_okay_test!(partition);
eval_okay_test!(path);
eval_okay_test!(pathexists);
eval_okay_test!(path_string_interpolation);
eval_okay_test!(patterns);
eval_okay_test!(print);
eval_okay_test!(readDir);
eval_okay_test!(readfile);
eval_okay_test!(readFileType);
eval_okay_test!(redefine_builtin);
eval_okay_test!(regex_match);
eval_okay_test!(regex_split);
eval_okay_test!(regression_20220122);
eval_okay_test!(regression_20220125);
eval_okay_test!(regrettable_rec_attrset_merge);
eval_okay_test!(remove);
eval_okay_test!(repeated_empty_attrs);
eval_okay_test!(repeated_empty_list);
eval_okay_test!(replacestrings);
eval_okay_test!(#[ignore = "requires -I CLI flags"] search_path);
eval_okay_test!(scope_1);
eval_okay_test!(scope_2);
eval_okay_test!(scope_3);
eval_okay_test!(scope_4);
eval_okay_test!(scope_6);
eval_okay_test!(scope_7);
eval_okay_test!(seq);
eval_okay_test!(sort);
eval_okay_test!(splitversion);
eval_okay_test!(string);
eval_okay_test!(strings_as_attrs_names);
eval_okay_test!(substring);
eval_okay_test!(substring_context);
eval_okay_test!(symlink_resolution);
eval_okay_test!(tail_call_1);
eval_okay_test!(tojson);
eval_okay_test!(#[ignore = "not implemented: toXML"] toxml);
eval_okay_test!(#[ignore = "not implemented: toXML"] toxml2);
eval_okay_test!(tryeval);
eval_okay_test!(types);
eval_okay_test!(versions);
eval_okay_test!(with);
eval_okay_test!(#[ignore = "not implemented: hashString"] zipAttrsWith);
eval_fail_test!(fail_abort);
eval_fail_test!(fail_addDrvOutputDependencies_empty_context);
eval_fail_test!(fail_addDrvOutputDependencies_multi_elem_context);
eval_fail_test!(fail_addDrvOutputDependencies_wrong_element_kind);
eval_fail_test!(fail_addErrorContext_example);
eval_fail_test!(fail_assert);
eval_fail_test!(fail_assert_equal_attrs_names);
eval_fail_test!(fail_assert_equal_attrs_names_2);
eval_fail_test!(fail_assert_equal_derivations);
eval_fail_test!(fail_assert_equal_derivations_extra);
eval_fail_test!(fail_assert_equal_floats);
eval_fail_test!(fail_assert_equal_function_direct);
eval_fail_test!(fail_assert_equal_int_float);
eval_fail_test!(fail_assert_equal_ints);
eval_fail_test!(fail_assert_equal_list_length);
eval_fail_test!(fail_assert_equal_paths);
eval_fail_test!(fail_assert_equal_type);
eval_fail_test!(fail_assert_equal_type_nested);
eval_fail_test!(fail_assert_nested_bool);
eval_fail_test!(fail_attr_name_type);
eval_fail_test!(fail_attrset_merge_drops_later_rec);
eval_fail_test!(fail_bad_string_interpolation_1);
eval_fail_test!(fail_bad_string_interpolation_2);
eval_fail_test!(fail_bad_string_interpolation_3);
eval_fail_test!(fail_bad_string_interpolation_4);
eval_fail_test!(fail_blackhole);
eval_fail_test!(fail_call_primop);
eval_fail_test!(fail_deepseq);
eval_fail_test!(fail_derivation_name);
eval_fail_test!(fail_dup_dynamic_attrs);
eval_fail_test!(fail_duplicate_traces);
eval_fail_test!(fail_eol_1);
eval_fail_test!(fail_eol_2);
eval_fail_test!(fail_eol_3);
eval_fail_test!(fail_fetchTree_negative);
eval_fail_test!(fail_fetchurl_baseName);
eval_fail_test!(fail_fetchurl_baseName_attrs);
eval_fail_test!(fail_fetchurl_baseName_attrs_name);
eval_fail_test!(fail_flake_ref_to_string_negative_integer);
eval_fail_test!(fail_foldlStrict_strict_op_application);
eval_fail_test!(fail_fromJSON_keyWithNullByte);
eval_fail_test!(fail_fromJSON_overflowing);
eval_fail_test!(fail_fromJSON_valueWithNullByte);
eval_fail_test!(fail_fromTOML_keyWithNullByte);
eval_fail_test!(fail_fromTOML_timestamps);
eval_fail_test!(fail_fromTOML_valueWithNullByte);
eval_fail_test!(fail_hashfile_missing);
eval_fail_test!(fail_infinite_recursion_lambda);
eval_fail_test!(fail_list);
eval_fail_test!(fail_missing_arg);
eval_fail_test!(fail_mutual_recursion);
eval_fail_test!(fail_nested_list_items);
eval_fail_test!(fail_nonexist_path);
eval_fail_test!(fail_not_throws);
eval_fail_test!(fail_overflowing_add);
eval_fail_test!(fail_overflowing_div);
eval_fail_test!(fail_overflowing_mul);
eval_fail_test!(fail_overflowing_sub);
eval_fail_test!(fail_path_slash);
eval_fail_test!(fail_pipe_operators);
eval_fail_test!(fail_recursion);
eval_fail_test!(fail_remove);
eval_fail_test!(fail_scope_5);
eval_fail_test!(fail_seq);
eval_fail_test!(fail_set);
eval_fail_test!(fail_set_override);
eval_fail_test!(fail_string_nul_1);
eval_fail_test!(fail_string_nul_2);
eval_fail_test!(fail_substring);
eval_fail_test!(fail_toJSON);
eval_fail_test!(fail_toJSON_non_utf_8);
eval_fail_test!(fail_to_path);
eval_fail_test!(fail_undeclared_arg);
eval_fail_test!(fail_using_set_as_attr_name);

View File

@@ -0,0 +1 @@
"a"

View File

@@ -0,0 +1 @@
"X"

View File

@@ -0,0 +1 @@
"b"

View File

@@ -0,0 +1 @@
"X"

View File

@@ -0,0 +1 @@
"X"

View File

@@ -0,0 +1 @@
"c"

View File

@@ -0,0 +1 @@
"X"

View File

@@ -0,0 +1 @@
"X"

View File

@@ -0,0 +1,8 @@
error:
… while calling the 'abort' builtin
at /pwd/lang/eval-fail-abort.nix:1:14:
1| if true then abort "this should fail" else 1
| ^
2|
error: evaluation aborted with the following error message: 'this should fail'

View File

@@ -0,0 +1 @@
if true then abort "this should fail" else 1

View File

@@ -0,0 +1,8 @@
error:
… while calling the 'addDrvOutputDependencies' builtin
at /pwd/lang/eval-fail-addDrvOutputDependencies-empty-context.nix:1:1:
1| builtins.addDrvOutputDependencies ""
| ^
2|
error: context of string '' must have exactly one element, but has 0

View File

@@ -0,0 +1 @@
builtins.addDrvOutputDependencies ""

View File

@@ -0,0 +1,9 @@
error:
… while calling the 'addDrvOutputDependencies' builtin
at /pwd/lang/eval-fail-addDrvOutputDependencies-multi-elem-context.nix:25:1:
24| in
25| builtins.addDrvOutputDependencies combo-path
| ^
26|
error: context of string '/nix/store/pg9yqs4yd85yhdm3f4i5dyaqp5jahrsz-fail.drv/nix/store/2dxd5frb715z451vbf7s8birlf3argbk-fail-2.drv' must have exactly one element, but has 2

View File

@@ -0,0 +1,25 @@
let
drv0 = derivation {
name = "fail";
builder = "/bin/false";
system = "x86_64-linux";
outputs = [
"out"
"foo"
];
};
drv1 = derivation {
name = "fail-2";
builder = "/bin/false";
system = "x86_64-linux";
outputs = [
"out"
"foo"
];
};
combo-path = "${drv0.drvPath}${drv1.drvPath}";
in
builtins.addDrvOutputDependencies combo-path

View File

@@ -0,0 +1,9 @@
error:
… while calling the 'addDrvOutputDependencies' builtin
at /pwd/lang/eval-fail-addDrvOutputDependencies-wrong-element-kind.nix:13:1:
12| in
13| builtins.addDrvOutputDependencies drv.outPath
| ^
14|
error: `addDrvOutputDependencies` can only act on derivations, not on a derivation output such as 'out'

View File

@@ -0,0 +1,13 @@
let
drv = derivation {
name = "fail";
builder = "/bin/false";
system = "x86_64-linux";
outputs = [
"out"
"foo"
];
};
in
builtins.addDrvOutputDependencies drv.outPath

View File

@@ -0,0 +1,24 @@
error:
… while counting down; n = 10
… while counting down; n = 9
… while counting down; n = 8
… while counting down; n = 7
… while counting down; n = 6
… while counting down; n = 5
… while counting down; n = 4
… while counting down; n = 3
… while counting down; n = 2
… while counting down; n = 1
(stack trace truncated; use '--show-trace' to show the full, detailed trace)
error: kaboom

View File

@@ -0,0 +1,9 @@
let
countDown =
n:
if n == 0 then
throw "kaboom"
else
builtins.addErrorContext "while counting down; n = ${toString n}" ("x" + countDown (n - 1));
in
countDown 10

View File

@@ -0,0 +1,8 @@
error:
… while evaluating the condition of the assertion '({ a = true; } == { a = true; b = true; })'
at /pwd/lang/eval-fail-assert-equal-attrs-names-2.nix:1:1:
1| assert
| ^
2| {
error: attribute names of attribute set '{ a = true; }' differs from attribute set '{ a = true; b = true; }'

View File

@@ -0,0 +1,8 @@
assert
{
a = true;
} == {
a = true;
b = true;
};
throw "unreachable"

View File

@@ -0,0 +1,8 @@
error:
… while evaluating the condition of the assertion '({ a = true; b = true; } == { a = true; })'
at /pwd/lang/eval-fail-assert-equal-attrs-names.nix:1:1:
1| assert
| ^
2| {
error: attribute names of attribute set '{ a = true; b = true; }' differs from attribute set '{ a = true; }'

View File

@@ -0,0 +1,8 @@
assert
{
a = true;
b = true;
} == {
a = true;
};
throw "unreachable"

View File

@@ -0,0 +1,26 @@
error:
… while evaluating the condition of the assertion '({ foo = { outPath = "/nix/store/0"; type = "derivation"; }; } == { foo = { devious = true; outPath = "/nix/store/1"; type = "derivation"; }; })'
at /pwd/lang/eval-fail-assert-equal-derivations-extra.nix:1:1:
1| assert
| ^
2| {
… while comparing attribute 'foo'
… where left hand side is
at /pwd/lang/eval-fail-assert-equal-derivations-extra.nix:3:5:
2| {
3| foo = {
| ^
4| type = "derivation";
… where right hand side is
at /pwd/lang/eval-fail-assert-equal-derivations-extra.nix:8:5:
7| } == {
8| foo = {
| ^
9| type = "derivation";
… while comparing a derivation by its 'outPath' attribute
error: string '"/nix/store/0"' is not equal to string '"/nix/store/1"'

View File

@@ -0,0 +1,14 @@
assert
{
foo = {
type = "derivation";
outPath = "/nix/store/0";
};
} == {
foo = {
type = "derivation";
outPath = "/nix/store/1";
devious = true;
};
};
throw "unreachable"

View File

@@ -0,0 +1,26 @@
error:
… while evaluating the condition of the assertion '({ foo = { ignored = (abort "not ignored"); outPath = "/nix/store/0"; type = "derivation"; }; } == { foo = { ignored = (abort "not ignored"); outPath = "/nix/store/1"; type = "derivation"; }; })'
at /pwd/lang/eval-fail-assert-equal-derivations.nix:1:1:
1| assert
| ^
2| {
… while comparing attribute 'foo'
… where left hand side is
at /pwd/lang/eval-fail-assert-equal-derivations.nix:3:5:
2| {
3| foo = {
| ^
4| type = "derivation";
… where right hand side is
at /pwd/lang/eval-fail-assert-equal-derivations.nix:9:5:
8| } == {
9| foo = {
| ^
10| type = "derivation";
… while comparing a derivation by its 'outPath' attribute
error: string '"/nix/store/0"' is not equal to string '"/nix/store/1"'

View File

@@ -0,0 +1,15 @@
assert
{
foo = {
type = "derivation";
outPath = "/nix/store/0";
ignored = abort "not ignored";
};
} == {
foo = {
type = "derivation";
outPath = "/nix/store/1";
ignored = abort "not ignored";
};
};
throw "unreachable"

View File

@@ -0,0 +1,22 @@
error:
… while evaluating the condition of the assertion '({ b = 1; } == { b = 1.01; })'
at /pwd/lang/eval-fail-assert-equal-floats.nix:1:1:
1| assert { b = 1.0; } == { b = 1.01; };
| ^
2| abort "unreachable"
… while comparing attribute 'b'
… where left hand side is
at /pwd/lang/eval-fail-assert-equal-floats.nix:1:10:
1| assert { b = 1.0; } == { b = 1.01; };
| ^
2| abort "unreachable"
… where right hand side is
at /pwd/lang/eval-fail-assert-equal-floats.nix:1:26:
1| assert { b = 1.0; } == { b = 1.01; };
| ^
2| abort "unreachable"
error: a float with value '1' is not equal to a float with value '1.01'

View File

@@ -0,0 +1,2 @@
assert { b = 1.0; } == { b = 1.01; };
abort "unreachable"

View File

@@ -0,0 +1,9 @@
error:
… while evaluating the condition of the assertion '((x: x) == (x: x))'
at /pwd/lang/eval-fail-assert-equal-function-direct.nix:3:1:
2| # This only compares a direct comparison and makes no claims about functions in nested structures.
3| assert (x: x) == (x: x);
| ^
4| abort "unreachable"
error: distinct functions and immediate comparisons of identical functions compare as unequal

View File

@@ -0,0 +1,4 @@
# Note: functions in nested structures, e.g. attributes, may be optimized away by pointer identity optimization.
# This only compares a direct comparison and makes no claims about functions in nested structures.
assert (x: x) == (x: x);
abort "unreachable"

View File

@@ -0,0 +1,8 @@
error:
… while evaluating the condition of the assertion '(1 == 1.1)'
at /pwd/lang/eval-fail-assert-equal-int-float.nix:1:1:
1| assert 1 == 1.1;
| ^
2| throw "unreachable"
error: an integer with value '1' is not equal to a float with value '1.1'

View File

@@ -0,0 +1,2 @@
assert 1 == 1.1;
throw "unreachable"

View File

@@ -0,0 +1,22 @@
error:
… while evaluating the condition of the assertion '({ b = 1; } == { b = 2; })'
at /pwd/lang/eval-fail-assert-equal-ints.nix:1:1:
1| assert { b = 1; } == { b = 2; };
| ^
2| abort "unreachable"
… while comparing attribute 'b'
… where left hand side is
at /pwd/lang/eval-fail-assert-equal-ints.nix:1:10:
1| assert { b = 1; } == { b = 2; };
| ^
2| abort "unreachable"
… where right hand side is
at /pwd/lang/eval-fail-assert-equal-ints.nix:1:24:
1| assert { b = 1; } == { b = 2; };
| ^
2| abort "unreachable"
error: an integer with value '1' is not equal to an integer with value '2'

View File

@@ -0,0 +1,2 @@
assert { b = 1; } == { b = 2; };
abort "unreachable"

View File

@@ -0,0 +1,8 @@
error:
… while evaluating the condition of the assertion '([ (1) (0) ] == [ (10) ])'
at /pwd/lang/eval-fail-assert-equal-list-length.nix:1:1:
1| assert
| ^
2| [
error: list of size '2' is not equal to list of size '1', left hand side is '[ 1 0 ]', right hand side is '[ 10 ]'

View File

@@ -0,0 +1,6 @@
assert
[
1
0
] == [ 10 ];
throw "unreachable"

View File

@@ -0,0 +1,8 @@
error:
… while evaluating the condition of the assertion '(/pwd/lang/foo == /pwd/lang/bar)'
at /pwd/lang/eval-fail-assert-equal-paths.nix:1:1:
1| assert ./foo == ./bar;
| ^
2| throw "unreachable"
error: path '/pwd/lang/foo' is not equal to path '/pwd/lang/bar'

View File

@@ -0,0 +1,2 @@
assert ./foo == ./bar;
throw "unreachable"

View File

@@ -0,0 +1,22 @@
error:
… while evaluating the condition of the assertion '({ ding = false; } == { ding = null; })'
at /pwd/lang/eval-fail-assert-equal-type-nested.nix:1:1:
1| assert { ding = false; } == { ding = null; };
| ^
2| abort "unreachable"
… while comparing attribute 'ding'
… where left hand side is
at /pwd/lang/eval-fail-assert-equal-type-nested.nix:1:10:
1| assert { ding = false; } == { ding = null; };
| ^
2| abort "unreachable"
… where right hand side is
at /pwd/lang/eval-fail-assert-equal-type-nested.nix:1:31:
1| assert { ding = false; } == { ding = null; };
| ^
2| abort "unreachable"
error: a Boolean of value 'false' is not equal to null of value 'null'

View File

@@ -0,0 +1,2 @@
assert { ding = false; } == { ding = null; };
abort "unreachable"

View File

@@ -0,0 +1,8 @@
error:
… while evaluating the condition of the assertion '(false == null)'
at /pwd/lang/eval-fail-assert-equal-type.nix:1:1:
1| assert false == null;
| ^
2| abort "unreachable"
error: a Boolean of value 'false' is not equal to null of value 'null'

View File

@@ -0,0 +1,2 @@
assert false == null;
abort "unreachable"

View File

@@ -0,0 +1,66 @@
error:
… while evaluating the condition of the assertion '({ a = { b = [ ({ c = { d = true; }; }) ]; }; } == { a = { b = [ ({ c = { d = false; }; }) ]; }; })'
at /pwd/lang/eval-fail-assert-nested-bool.nix:1:1:
1| assert { a.b = [ { c.d = true; } ]; } == { a.b = [ { c.d = false; } ]; };
| ^
2|
… while comparing attribute 'a'
… where left hand side is
at /pwd/lang/eval-fail-assert-nested-bool.nix:1:10:
1| assert { a.b = [ { c.d = true; } ]; } == { a.b = [ { c.d = false; } ]; };
| ^
2|
… where right hand side is
at /pwd/lang/eval-fail-assert-nested-bool.nix:1:44:
1| assert { a.b = [ { c.d = true; } ]; } == { a.b = [ { c.d = false; } ]; };
| ^
2|
… while comparing attribute 'b'
… where left hand side is
at /pwd/lang/eval-fail-assert-nested-bool.nix:1:10:
1| assert { a.b = [ { c.d = true; } ]; } == { a.b = [ { c.d = false; } ]; };
| ^
2|
… where right hand side is
at /pwd/lang/eval-fail-assert-nested-bool.nix:1:44:
1| assert { a.b = [ { c.d = true; } ]; } == { a.b = [ { c.d = false; } ]; };
| ^
2|
… while comparing list element 0
… while comparing attribute 'c'
… where left hand side is
at /pwd/lang/eval-fail-assert-nested-bool.nix:1:20:
1| assert { a.b = [ { c.d = true; } ]; } == { a.b = [ { c.d = false; } ]; };
| ^
2|
… where right hand side is
at /pwd/lang/eval-fail-assert-nested-bool.nix:1:54:
1| assert { a.b = [ { c.d = true; } ]; } == { a.b = [ { c.d = false; } ]; };
| ^
2|
… while comparing attribute 'd'
… where left hand side is
at /pwd/lang/eval-fail-assert-nested-bool.nix:1:20:
1| assert { a.b = [ { c.d = true; } ]; } == { a.b = [ { c.d = false; } ]; };
| ^
2|
… where right hand side is
at /pwd/lang/eval-fail-assert-nested-bool.nix:1:54:
1| assert { a.b = [ { c.d = true; } ]; } == { a.b = [ { c.d = false; } ]; };
| ^
2|
error: boolean 'true' is not equal to boolean 'false'

View File

@@ -0,0 +1,3 @@
assert { a.b = [ { c.d = true; } ]; } == { a.b = [ { c.d = false; } ]; };
abort "unreachable"

View File

@@ -0,0 +1,30 @@
error:
… while evaluating the attribute 'body'
at /pwd/lang/eval-fail-assert.nix:7:3:
6|
7| body = x "x";
| ^
8| }
… from call site
at /pwd/lang/eval-fail-assert.nix:7:10:
6|
7| body = x "x";
| ^
8| }
… while calling 'x'
at /pwd/lang/eval-fail-assert.nix:3:5:
2| x =
3| arg:
| ^
4| assert arg == "y";
… while evaluating the condition of the assertion '(arg == "y")'
at /pwd/lang/eval-fail-assert.nix:4:5:
3| arg:
4| assert arg == "y";
| ^
5| 123;
error: string '"x"' is not equal to string '"y"'

View File

@@ -0,0 +1,8 @@
let {
x =
arg:
assert arg == "y";
123;
body = x "x";
}

View File

@@ -0,0 +1,21 @@
error:
… while evaluating the attribute 'puppy."${key}"'
at /pwd/lang/eval-fail-attr-name-type.nix:3:5:
2| attrs = {
3| puppy.doggy = { };
| ^
4| };
… while evaluating an attribute name
at /pwd/lang/eval-fail-attr-name-type.nix:7:15:
6| in
7| attrs.puppy.${key}
| ^
8|
error: expected a string but found an integer: 1
at /pwd/lang/eval-fail-attr-name-type.nix:7:15:
6| in
7| attrs.puppy.${key}
| ^
8|

View File

@@ -0,0 +1,7 @@
let
attrs = {
puppy.doggy = { };
};
key = 1;
in
attrs.puppy.${key}

View File

@@ -0,0 +1,6 @@
error: undefined variable 'd'
at /pwd/lang/eval-fail-attrset-merge-drops-later-rec.nix:4:9:
3| a = rec {
4| c = d + 2;
| ^
5| d = 3;

View File

@@ -0,0 +1,8 @@
{
a.b = 1;
a = rec {
c = d + 2;
d = 3;
};
}
.c

View File

@@ -0,0 +1,8 @@
error:
… while evaluating a path segment
at /pwd/lang/eval-fail-bad-string-interpolation-1.nix:1:2:
1| "${x: x}"
| ^
2|
error: cannot coerce a function to a string: «lambda @ /pwd/lang/eval-fail-bad-string-interpolation-1.nix:1:4»

View File

@@ -0,0 +1 @@
"${x: x}"

View File

@@ -0,0 +1 @@
error: path '/pwd/lang/fnord' does not exist

View File

@@ -0,0 +1 @@
"${./fnord}"

View File

@@ -0,0 +1,8 @@
error:
… while evaluating a path segment
at /pwd/lang/eval-fail-bad-string-interpolation-3.nix:1:3:
1| ''${x: x}''
| ^
2|
error: cannot coerce a function to a string: «lambda @ /pwd/lang/eval-fail-bad-string-interpolation-3.nix:1:5»

View File

@@ -0,0 +1 @@
''${x: x}''

View File

@@ -0,0 +1,9 @@
error:
… while evaluating a path segment
at /pwd/lang/eval-fail-bad-string-interpolation-4.nix:19:3:
18| # The error message should not be too long.
19| ''${pkgs}''
| ^
20|
error: cannot coerce a set to a string: { a = { a = { a = { a = "ha"; b = "ha"; c = "ha"; d = "ha"; e = "ha"; f = "ha"; g = "ha"; h = "ha"; j = "ha"; }; «8 attributes elided» }; «8 attributes elided» }; «8 attributes elided» }

View File

@@ -0,0 +1,19 @@
let
# Basically a "billion laughs" attack, but toned down to simulated `pkgs`.
ha = x: y: {
a = x y;
b = x y;
c = x y;
d = x y;
e = x y;
f = x y;
g = x y;
h = x y;
j = x y;
};
has = ha (ha (ha (ha (x: x)))) "ha";
# A large structure that has already been evaluated.
pkgs = builtins.deepSeq has has;
in
# The error message should not be too long.
''${pkgs}''

View File

@@ -0,0 +1,14 @@
error:
… while evaluating the attribute 'body'
at /pwd/lang/eval-fail-blackhole.nix:2:3:
1| let {
2| body = x;
| ^
3| x = y;
error: infinite recursion encountered
at /pwd/lang/eval-fail-blackhole.nix:3:7:
2| body = x;
3| x = y;
| ^
4| y = x;

View File

@@ -0,0 +1,5 @@
let {
body = x;
x = y;
y = x;
}

View File

@@ -0,0 +1,10 @@
error:
… while calling the 'length' builtin
at /pwd/lang/eval-fail-call-primop.nix:1:1:
1| builtins.length 1
| ^
2|
… while evaluating the first argument passed to builtins.length
error: expected a list but found an integer: 1

View File

@@ -0,0 +1 @@
builtins.length 1

View File

@@ -0,0 +1,20 @@
error:
… while calling the 'deepSeq' builtin
at /pwd/lang/eval-fail-deepseq.nix:1:1:
1| builtins.deepSeq { x = abort "foo"; } 456
| ^
2|
… while evaluating the attribute 'x'
at /pwd/lang/eval-fail-deepseq.nix:1:20:
1| builtins.deepSeq { x = abort "foo"; } 456
| ^
2|
… while calling the 'abort' builtin
at /pwd/lang/eval-fail-deepseq.nix:1:24:
1| builtins.deepSeq { x = abort "foo"; } 456
| ^
2|
error: evaluation aborted with the following error message: 'foo'

View File

@@ -0,0 +1 @@
builtins.deepSeq { x = abort "foo"; } 456

View File

@@ -0,0 +1,26 @@
error:
… while evaluating the attribute 'outPath'
at <nix/derivation-internal.nix>:<number>:<number>:
<number>| value = commonAttrs // {
<number>| outPath = builtins.getAttr outputName strict;
| ^
<number>| drvPath = strict.drvPath;
… while calling the 'getAttr' builtin
at <nix/derivation-internal.nix>:<number>:<number>:
<number>| value = commonAttrs // {
<number>| outPath = builtins.getAttr outputName strict;
| ^
<number>| drvPath = strict.drvPath;
… while calling the 'derivationStrict' builtin
at <nix/derivation-internal.nix>:<number>:<number>:
<number>|
<number>| strict = derivationStrict drvAttrs;
| ^
<number>|
… while evaluating derivation '~jiggle~'
whose name attribute is located at /pwd/lang/eval-fail-derivation-name.nix:<number>:<number>
error: invalid derivation name: name '~jiggle~' contains illegal character '~'. Please pass a different 'name'.

View File

@@ -0,0 +1,5 @@
derivation {
name = "~jiggle~";
system = "some-system";
builder = "/dontcare";
}

View File

@@ -0,0 +1,14 @@
error:
… while evaluating the attribute 'set'
at /pwd/lang/eval-fail-dup-dynamic-attrs.nix:2:3:
1| {
2| set = {
| ^
3| "${"" + "b"}" = 1;
error: dynamic attribute 'b' already defined at /pwd/lang/eval-fail-dup-dynamic-attrs.nix:3:5
at /pwd/lang/eval-fail-dup-dynamic-attrs.nix:6:5:
5| set = {
6| "${"b" + ""}" = 2;
| ^
7| };

View File

@@ -0,0 +1,8 @@
{
set = {
"${"" + "b"}" = 1;
};
set = {
"${"b" + ""}" = 2;
};
}

View File

@@ -0,0 +1,51 @@
error:
… from call site
at /pwd/lang/eval-fail-duplicate-traces.nix:6:1:
5| in
6| throwAfter 2
| ^
7|
… while calling 'throwAfter'
at /pwd/lang/eval-fail-duplicate-traces.nix:4:16:
3| let
4| throwAfter = n: if n > 0 then throwAfter (n - 1) else throw "Uh oh!";
| ^
5| in
… from call site
at /pwd/lang/eval-fail-duplicate-traces.nix:4:33:
3| let
4| throwAfter = n: if n > 0 then throwAfter (n - 1) else throw "Uh oh!";
| ^
5| in
… while calling 'throwAfter'
at /pwd/lang/eval-fail-duplicate-traces.nix:4:16:
3| let
4| throwAfter = n: if n > 0 then throwAfter (n - 1) else throw "Uh oh!";
| ^
5| in
… from call site
at /pwd/lang/eval-fail-duplicate-traces.nix:4:33:
3| let
4| throwAfter = n: if n > 0 then throwAfter (n - 1) else throw "Uh oh!";
| ^
5| in
… while calling 'throwAfter'
at /pwd/lang/eval-fail-duplicate-traces.nix:4:16:
3| let
4| throwAfter = n: if n > 0 then throwAfter (n - 1) else throw "Uh oh!";
| ^
5| in
… while calling the 'throw' builtin
at /pwd/lang/eval-fail-duplicate-traces.nix:4:57:
3| let
4| throwAfter = n: if n > 0 then throwAfter (n - 1) else throw "Uh oh!";
| ^
5| in
error: Uh oh!

View File

@@ -0,0 +1,6 @@
# Check that we only omit duplicate stack traces when there's a bunch of them.
# Here, there's only a couple duplicate entries, so we output them all.
let
throwAfter = n: if n > 0 then throwAfter (n - 1) else throw "Uh oh!";
in
throwAfter 2

View File

@@ -0,0 +1,6 @@
error: undefined variable 'invalid'
at /pwd/lang/eval-fail-eol-1.nix:2:1:
1| # foo
2| invalid
| ^
3| # bar

View File

@@ -0,0 +1,3 @@
# foo
invalid
# bar

View File

@@ -0,0 +1,6 @@
error: undefined variable 'invalid'
at /pwd/lang/eval-fail-eol-2.nix:2:1:
1| # foo
2| invalid
| ^
3| # bar

View File

@@ -0,0 +1,2 @@
# foo
invalid

View File

@@ -0,0 +1,6 @@
error: undefined variable 'invalid'
at /pwd/lang/eval-fail-eol-3.nix:2:1:
1| # foo
2| invalid
| ^
3| # bar

View File

@@ -0,0 +1,3 @@
# foo
invalid
# bar

View File

@@ -0,0 +1,8 @@
error:
… while calling the 'fetchTree' builtin
at /pwd/lang/eval-fail-fetchTree-negative.nix:1:1:
1| builtins.fetchTree {
| ^
2| type = "file";
error: negative value given for 'fetchTree' argument 'owner': -1

View File

@@ -0,0 +1,5 @@
builtins.fetchTree {
type = "file";
url = "file://eval-fail-fetchTree-negative.nix";
owner = -1;
}

View File

@@ -0,0 +1,8 @@
error:
… while calling the 'fetchurl' builtin
at /pwd/lang/eval-fail-fetchurl-baseName-attrs-name.nix:1:1:
1| builtins.fetchurl {
| ^
2| url = "https://example.com/foo.tar.gz";
error: invalid store path name when fetching URL 'https://example.com/foo.tar.gz': name '~wobble~' contains illegal character '~'. Please change the value for the 'name' attribute passed to 'fetchurl', so that it can create a valid store path.

View File

@@ -0,0 +1,4 @@
builtins.fetchurl {
url = "https://example.com/foo.tar.gz";
name = "~wobble~";
}

View File

@@ -0,0 +1,8 @@
error:
… while calling the 'fetchurl' builtin
at /pwd/lang/eval-fail-fetchurl-baseName-attrs.nix:1:1:
1| builtins.fetchurl { url = "https://example.com/~wiggle~"; }
| ^
2|
error: invalid store path name when fetching URL 'https://example.com/~wiggle~': name '~wiggle~' contains illegal character '~'. Please add a valid 'name' attribute to the argument for 'fetchurl', so that it can create a valid store path.

View File

@@ -0,0 +1 @@
builtins.fetchurl { url = "https://example.com/~wiggle~"; }

View File

@@ -0,0 +1,8 @@
error:
… while calling the 'fetchurl' builtin
at /pwd/lang/eval-fail-fetchurl-baseName.nix:1:1:
1| builtins.fetchurl "https://example.com/~wiggle~"
| ^
2|
error: invalid store path name when fetching URL 'https://example.com/~wiggle~': name '~wiggle~' contains illegal character '~'. Please pass an attribute set with 'url' and 'name' attributes to 'fetchurl', so that it can create a valid store path.

Some files were not shown because too many files have changed in this diff Show More