feat: thunk caching (WIP)

This commit is contained in:
2026-02-21 23:23:56 +08:00
parent 550223a1d7
commit d7351e907b
13 changed files with 403 additions and 47 deletions

View File

@@ -89,3 +89,4 @@ globalThis.$og = op.gt;
globalThis.$oc = op.concat;
globalThis.$ou = op.update;
globalThis.$b = builtins;
globalThis.$e = new Map();

View File

@@ -43,11 +43,6 @@ export const printValue = (value: NixValue, seen: WeakSet<object> = new WeakSet(
return "«repeated»";
}
if (seen.has(value)) {
return "«repeated»";
}
seen.add(value);
if (isNixPath(value)) {
return value.value;
}
@@ -57,10 +52,23 @@ export const printValue = (value: NixValue, seen: WeakSet<object> = new WeakSet(
}
if (Array.isArray(value)) {
if (value.length > 0) {
if (seen.has(value)) {
return "«repeated»";
}
seen.add(value);
}
const items = value.map((v) => printValue(v, seen)).join(" ");
return `[ ${items} ]`;
}
if (seen.has(value)) {
return "«repeated»";
}
if (value.size > 0) {
seen.add(value);
}
const entries = [...value.entries()]
.map(([k, v]) => `${printSymbol(k)} = ${printValue(v, seen)};`)
.join(" ");

View File

@@ -132,12 +132,9 @@ export const forceDeep = (value: NixValue, seen: WeakSet<object> = new WeakSet()
}
if (seen.has(forced)) {
if (Array.isArray(forced)) {
return [CYCLE_MARKER];
}
return CYCLE_MARKER;
}
if (isAttrs(forced) || isList(forced)) {
if ((isAttrs(forced) && forced.size > 0) || (isList(forced) && forced.length > 0)) {
seen.add(forced);
}

View File

@@ -43,6 +43,7 @@ declare global {
var $oc: typeof op.concat;
var $ou: typeof op.update;
var $b: typeof builtins;
var $e: NixAttrs;
var $gb: typeof Nix.getReplBinding;
namespace Deno {

View File

@@ -294,7 +294,7 @@ impl<Ctx: CodegenContext> Compile<Ctx> for Ir<'_> {
")"
);
}
Ir::WithExpr(x) => x.compile(ctx, buf),
Ir::With(x) => x.compile(ctx, buf),
&Ir::WithLookup(WithLookup { inner: name, .. }) => {
// Nix.lookupWith
code!(buf, ctx;
@@ -487,7 +487,7 @@ impl<Ctx: CodegenContext> Compile<Ctx> for TopLevel<'_> {
}
}
impl<Ctx: CodegenContext> Compile<Ctx> for WithExpr<'_> {
impl<Ctx: CodegenContext> Compile<Ctx> for With<'_> {
fn compile(&self, ctx: &Ctx, buf: &mut CodeBuffer) {
let namespace = self.namespace;
let body = self.body;
@@ -585,7 +585,7 @@ impl<Ctx: CodegenContext> Compile<Ctx> for AttrSet<'_> {
"]))"
);
} else {
code!(buf, ctx; "new Map()");
code!(buf, ctx; "$e");
}
}
}

View File

@@ -2,7 +2,7 @@ use std::cell::UnsafeCell;
use std::path::Path;
use bumpalo::Bump;
use hashbrown::{HashMap, HashSet};
use hashbrown::{DefaultHashBuilder, HashMap, HashSet};
use rnix::TextRange;
use string_interner::DefaultStringInterner;
@@ -10,8 +10,8 @@ use crate::codegen::{CodegenContext, compile, compile_scoped};
use crate::downgrade::*;
use crate::error::{Error, Result, Source};
use crate::ir::{
Arg, ArgId, Bool, Builtin, Ir, IrRef, Null, ReplBinding, ScopedImportBinding, SymId, Thunk,
ThunkId, ToIr as _, WithLookup,
Arg, ArgId, Bool, Builtin, Ir, IrKey, IrRef, Null, ReplBinding, ScopedImportBinding, SymId,
Thunk, ThunkId, ToIr as _, WithLookup,
};
#[cfg(feature = "inspector")]
use crate::runtime::inspector::InspectorServer;
@@ -450,6 +450,38 @@ impl<'ir, 'ctx> ScopeGuard<'_, 'ctx, 'ir> {
}
}
struct ThunkScope<'ir> {
bindings: bumpalo::collections::Vec<'ir, (ThunkId, IrRef<'ir>)>,
cache: HashMap<IrKey<'ir>, ThunkId>,
hasher: DefaultHashBuilder,
}
impl<'ir> ThunkScope<'ir> {
fn new_in(bump: &'ir Bump) -> Self {
Self {
bindings: bumpalo::collections::Vec::new_in(bump),
cache: HashMap::new(),
hasher: DefaultHashBuilder::default(),
}
}
fn lookup_cache(&self, key: IrKey<'ir>) -> Option<ThunkId> {
self.cache.get(&key).copied()
}
fn add_binding(&mut self, id: ThunkId, ir: IrRef<'ir>) {
self.bindings.push((id, ir));
}
fn extend_bindings(&mut self, iter: impl IntoIterator<Item = (ThunkId, IrRef<'ir>)>) {
self.bindings.extend(iter);
}
fn add_cache(&mut self, key: IrKey<'ir>, cache: ThunkId) {
self.cache.insert(key, cache);
}
}
struct DowngradeCtx<'ctx, 'ir> {
bump: &'ir Bump,
symbols: &'ctx mut DefaultStringInterner,
@@ -458,7 +490,7 @@ struct DowngradeCtx<'ctx, 'ir> {
with_scope_count: usize,
arg_count: usize,
thunk_count: &'ctx mut usize,
thunk_scopes: Vec<bumpalo::collections::Vec<'ir, (ThunkId, IrRef<'ir>)>>,
thunk_scopes: Vec<ThunkScope<'ir>>,
}
fn should_thunk(ir: IrRef<'_>) -> bool {
@@ -494,7 +526,7 @@ impl<'ctx, 'ir> DowngradeCtx<'ctx, 'ir> {
thunk_count,
arg_count: 0,
with_scope_count: 0,
thunk_scopes: vec![bumpalo::collections::Vec::new_in(bump)],
thunk_scopes: vec![ThunkScope::new_in(bump)],
}
}
}
@@ -517,13 +549,22 @@ impl<'ctx: 'ir, 'ir> DowngradeContext<'ir> for DowngradeCtx<'ctx, 'ir> {
fn maybe_thunk(&mut self, ir: IrRef<'ir>) -> IrRef<'ir> {
if should_thunk(ir) {
let scope = self.thunk_scopes.last_mut().expect("no active cache scope");
let key = IrKey(ir);
if let Some(id) = scope.lookup_cache(key) {
return self.new_expr(
Thunk {
inner: id,
span: ir.span(),
}
.to_ir(),
);
}
let span = ir.span();
let id = ThunkId(*self.thunk_count);
*self.thunk_count += 1;
self.thunk_scopes
.last_mut()
.expect("no active thunk scope")
.push((id, ir));
scope.add_binding(id, ir);
scope.add_cache(key, id);
self.new_expr(Thunk { inner: id, span }.to_ir())
} else {
ir
@@ -601,7 +642,7 @@ impl<'ctx: 'ir, 'ir> DowngradeContext<'ir> for DowngradeCtx<'ctx, 'ir> {
};
assert_eq!(keys.len(), vals.len());
let scope = self.thunk_scopes.last_mut().expect("no active thunk scope");
scope.extend((base..base + keys.len()).map(ThunkId).zip(vals));
scope.extend_bindings((base..base + keys.len()).map(ThunkId).zip(vals));
Ok(ret)
}
@@ -631,12 +672,14 @@ impl<'ctx: 'ir, 'ir> DowngradeContext<'ir> for DowngradeCtx<'ctx, 'ir> {
where
F: FnOnce(&mut Self) -> R,
{
self.thunk_scopes
.push(bumpalo::collections::Vec::new_in(self.bump));
self.thunk_scopes.push(ThunkScope::new_in(self.bump));
let ret = f(self);
(
ret,
self.thunk_scopes.pop().expect("no thunk scope left???"),
self.thunk_scopes
.pop()
.expect("no thunk scope left???")
.bindings,
)
}
@@ -650,7 +693,11 @@ impl<'ir, 'ctx: 'ir> DowngradeCtx<'ctx, 'ir> {
use crate::ir::TopLevel;
let body = root.downgrade(&mut self)?;
let span = body.span();
let thunks = self.thunk_scopes.pop().expect("no thunk scope left???");
let thunks = self
.thunk_scopes
.pop()
.expect("no thunk scope left???")
.bindings;
Ok(self.new_expr(TopLevel { body, thunks, span }.to_ir()))
}
}

View File

@@ -447,7 +447,7 @@ impl<'ir, Ctx: DowngradeContext<'ir>> Downgrade<'ir, Ctx> for ast::With {
let body = body?;
Ok(ctx.new_expr(
WithExpr {
With {
namespace,
body,
thunks,

View File

@@ -1,3 +1,5 @@
use std::hash::{Hash, Hasher};
use bumpalo::{Bump, boxed::Box, collections::Vec};
use rnix::{TextRange, ast};
use string_interner::symbol::SymbolU32;
@@ -10,6 +12,7 @@ pub type IrRef<'ir> = &'ir Ir<'ir>;
ir! {
Ir<'ir>;
// Literals
Int(i64),
Float(f64),
Bool(bool),
@@ -17,27 +20,37 @@ ir! {
Str { inner: Box<'ir, String> },
AttrSet { stcs: HashMap<'ir, SymId, (IrRef<'ir>, TextRange)>, dyns: Vec<'ir, (IrRef<'ir>, IrRef<'ir>, TextRange)> },
List { items: Vec<'ir, IrRef<'ir>> },
HasAttr { lhs: IrRef<'ir>, rhs: Vec<'ir, Attr<'ir>> },
BinOp { lhs: IrRef<'ir>, rhs: IrRef<'ir>, kind: BinOpKind },
UnOp { rhs: IrRef<'ir>, kind: UnOpKind },
Select { expr: IrRef<'ir>, attrpath: Vec<'ir, Attr<'ir>>, default: Option<IrRef<'ir>> },
If { cond: IrRef<'ir>, consq: IrRef<'ir>, alter: IrRef<'ir> },
Call { func: IrRef<'ir>, arg: IrRef<'ir> },
Assert { assertion: IrRef<'ir>, expr: IrRef<'ir>, assertion_raw: String },
ConcatStrings { parts: Vec<'ir, IrRef<'ir>>, force_string: bool },
Path { expr: IrRef<'ir> },
ConcatStrings { parts: Vec<'ir, IrRef<'ir>>, force_string: bool },
// OPs
UnOp { rhs: IrRef<'ir>, kind: UnOpKind },
BinOp { lhs: IrRef<'ir>, rhs: IrRef<'ir>, kind: BinOpKind },
HasAttr { lhs: IrRef<'ir>, rhs: Vec<'ir, Attr<'ir>> },
Select { expr: IrRef<'ir>, attrpath: Vec<'ir, Attr<'ir>>, default: Option<IrRef<'ir>> },
// Conditionals
If { cond: IrRef<'ir>, consq: IrRef<'ir>, alter: IrRef<'ir> },
Assert { assertion: IrRef<'ir>, expr: IrRef<'ir>, assertion_raw: String },
With { namespace: IrRef<'ir>, body: IrRef<'ir>, thunks: Vec<'ir, (ThunkId, IrRef<'ir>)> },
WithLookup(SymId),
// Function related
Func { body: IrRef<'ir>, param: Option<Param<'ir>>, arg: IrRef<'ir>, thunks: Vec<'ir, (ThunkId, IrRef<'ir>)> },
TopLevel { body: IrRef<'ir>, thunks: Vec<'ir, (ThunkId, IrRef<'ir>)> },
Arg(ArgId),
Thunk(ThunkId),
Call { func: IrRef<'ir>, arg: IrRef<'ir> },
// Builtins
Builtins,
Builtin(SymId),
// Misc
TopLevel { body: IrRef<'ir>, thunks: Vec<'ir, (ThunkId, IrRef<'ir>)> },
Thunk(ThunkId),
CurPos,
ReplBinding(SymId),
ScopedImportBinding(SymId),
WithExpr { namespace: IrRef<'ir>, body: IrRef<'ir>, thunks: Vec<'ir, (ThunkId, IrRef<'ir>)> },
WithLookup(SymId),
}
#[repr(transparent)]
@@ -63,7 +76,7 @@ pub enum Attr<'ir> {
}
/// The kinds of binary operations supported in Nix.
#[derive(Clone, Debug)]
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub enum BinOpKind {
// Arithmetic
Add,
@@ -120,7 +133,7 @@ impl From<ast::BinOpKind> for BinOpKind {
}
/// The kinds of unary operations.
#[derive(Clone, Debug)]
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub enum UnOpKind {
Neg, // Negation (`-`)
Not, // Logical not (`!`)
@@ -142,3 +155,290 @@ pub struct Param<'ir> {
pub optional: Vec<'ir, (SymId, TextRange)>,
pub ellipsis: bool,
}
#[derive(Clone, Copy)]
pub(crate) struct IrKey<'ir>(pub IrRef<'ir>);
impl std::hash::Hash for IrKey<'_> {
fn hash<H: Hasher>(&self, state: &mut H) {
ir_content_hash(self.0, state);
}
}
impl PartialEq for IrKey<'_> {
fn eq(&self, other: &Self) -> bool {
ir_content_eq(self.0, other.0)
}
}
impl Eq for IrKey<'_> {}
fn attr_content_hash(attr: &Attr<'_>, state: &mut impl Hasher) {
core::mem::discriminant(attr).hash(state);
match attr {
Attr::Dynamic(expr, _) => ir_content_hash(expr, state),
Attr::Str(sym, _) => sym.hash(state),
}
}
fn attr_content_eq(a: &Attr<'_>, b: &Attr<'_>) -> bool {
match (a, b) {
(Attr::Dynamic(ae, _), Attr::Dynamic(be, _)) => ir_content_eq(ae, be),
(Attr::Str(a, _), Attr::Str(b, _)) => a == b,
_ => false,
}
}
fn param_content_hash(param: &Param<'_>, state: &mut impl Hasher) {
param.required.len().hash(state);
for (sym, _) in param.required.iter() {
sym.hash(state);
}
param.optional.len().hash(state);
for (sym, _) in param.optional.iter() {
sym.hash(state);
}
param.ellipsis.hash(state);
}
fn param_content_eq(a: &Param<'_>, b: &Param<'_>) -> bool {
a.ellipsis == b.ellipsis
&& a.required.len() == b.required.len()
&& a.optional.len() == b.optional.len()
&& a.required
.iter()
.zip(b.required.iter())
.all(|((a, _), (b, _))| a == b)
&& a.optional
.iter()
.zip(b.optional.iter())
.all(|((a, _), (b, _))| a == b)
}
fn thunks_content_hash(thunks: &[(ThunkId, IrRef<'_>)], state: &mut impl Hasher) {
thunks.len().hash(state);
for (id, ir) in thunks {
id.hash(state);
ir_content_hash(ir, state);
}
}
fn thunks_content_eq(a: &[(ThunkId, IrRef<'_>)], b: &[(ThunkId, IrRef<'_>)]) -> bool {
a.len() == b.len()
&& a.iter()
.zip(b.iter())
.all(|((ai, ae), (bi, be))| ai == bi && ir_content_eq(ae, be))
}
fn ir_content_hash(ir: &Ir<'_>, state: &mut impl Hasher) {
core::mem::discriminant(ir).hash(state);
match ir {
Ir::Int(x) => x.inner.hash(state),
Ir::Float(x) => x.inner.to_bits().hash(state),
Ir::Bool(x) => x.inner.hash(state),
Ir::Null(_) => {}
Ir::Str(x) => x.inner.hash(state),
Ir::AttrSet(x) => {
x.stcs.len().hash(state);
let mut combined: u64 = 0;
for (&key, (val, _)) in x.stcs.iter() {
let mut h = std::hash::DefaultHasher::new();
key.hash(&mut h);
ir_content_hash(val, &mut h);
combined = combined.wrapping_add(h.finish());
}
combined.hash(state);
x.dyns.len().hash(state);
for (k, v, _) in x.dyns.iter() {
ir_content_hash(k, state);
ir_content_hash(v, state);
}
}
Ir::List(x) => {
x.items.len().hash(state);
for item in x.items.iter() {
ir_content_hash(item, state);
}
}
Ir::HasAttr(x) => {
ir_content_hash(x.lhs, state);
x.rhs.len().hash(state);
for attr in x.rhs.iter() {
attr_content_hash(attr, state);
}
}
Ir::BinOp(x) => {
ir_content_hash(x.lhs, state);
ir_content_hash(x.rhs, state);
x.kind.hash(state);
}
Ir::UnOp(x) => {
ir_content_hash(x.rhs, state);
x.kind.hash(state);
}
Ir::Select(x) => {
ir_content_hash(x.expr, state);
x.attrpath.len().hash(state);
for attr in x.attrpath.iter() {
attr_content_hash(attr, state);
}
x.default.is_some().hash(state);
if let Some(d) = x.default {
ir_content_hash(d, state);
}
}
Ir::If(x) => {
ir_content_hash(x.cond, state);
ir_content_hash(x.consq, state);
ir_content_hash(x.alter, state);
}
Ir::Call(x) => {
ir_content_hash(x.func, state);
ir_content_hash(x.arg, state);
}
Ir::Assert(x) => {
ir_content_hash(x.assertion, state);
ir_content_hash(x.expr, state);
x.assertion_raw.hash(state);
}
Ir::ConcatStrings(x) => {
x.force_string.hash(state);
x.parts.len().hash(state);
for part in x.parts.iter() {
ir_content_hash(part, state);
}
}
Ir::Path(x) => ir_content_hash(x.expr, state),
Ir::Func(x) => {
ir_content_hash(x.body, state);
ir_content_hash(x.arg, state);
x.param.is_some().hash(state);
if let Some(p) = &x.param {
param_content_hash(p, state);
}
thunks_content_hash(&x.thunks, state);
}
Ir::TopLevel(x) => {
ir_content_hash(x.body, state);
thunks_content_hash(&x.thunks, state);
}
Ir::Arg(x) => x.inner.hash(state),
Ir::Thunk(x) => x.inner.hash(state),
Ir::Builtins(_) => {}
Ir::Builtin(x) => x.inner.hash(state),
Ir::CurPos(x) => x.span.hash(state),
Ir::ReplBinding(x) => x.inner.hash(state),
Ir::ScopedImportBinding(x) => x.inner.hash(state),
Ir::With(x) => {
ir_content_hash(x.namespace, state);
ir_content_hash(x.body, state);
thunks_content_hash(&x.thunks, state);
}
Ir::WithLookup(x) => x.inner.hash(state),
}
}
fn ir_content_eq(a: &Ir<'_>, b: &Ir<'_>) -> bool {
std::ptr::eq(a, b)
|| match (a, b) {
(Ir::Int(a), Ir::Int(b)) => a.inner == b.inner,
(Ir::Float(a), Ir::Float(b)) => a.inner.to_bits() == b.inner.to_bits(),
(Ir::Bool(a), Ir::Bool(b)) => a.inner == b.inner,
(Ir::Null(_), Ir::Null(_)) => true,
(Ir::Str(a), Ir::Str(b)) => *a.inner == *b.inner,
(Ir::AttrSet(a), Ir::AttrSet(b)) => {
a.stcs.len() == b.stcs.len()
&& a.dyns.len() == b.dyns.len()
&& a.stcs.iter().all(|(&k, (v, _))| {
b.stcs.get(&k).is_some_and(|(bv, _)| ir_content_eq(v, bv))
})
&& a.dyns
.iter()
.zip(b.dyns.iter())
.all(|((ak, av, _), (bk, bv, _))| {
ir_content_eq(ak, bk) && ir_content_eq(av, bv)
})
}
(Ir::List(a), Ir::List(b)) => {
a.items.len() == b.items.len()
&& a.items
.iter()
.zip(b.items.iter())
.all(|(a, b)| ir_content_eq(a, b))
}
(Ir::HasAttr(a), Ir::HasAttr(b)) => {
ir_content_eq(a.lhs, b.lhs)
&& a.rhs.len() == b.rhs.len()
&& a.rhs
.iter()
.zip(b.rhs.iter())
.all(|(a, b)| attr_content_eq(a, b))
}
(Ir::BinOp(a), Ir::BinOp(b)) => {
a.kind == b.kind && ir_content_eq(a.lhs, b.lhs) && ir_content_eq(a.rhs, b.rhs)
}
(Ir::UnOp(a), Ir::UnOp(b)) => a.kind == b.kind && ir_content_eq(a.rhs, b.rhs),
(Ir::Select(a), Ir::Select(b)) => {
ir_content_eq(a.expr, b.expr)
&& a.attrpath.len() == b.attrpath.len()
&& a.attrpath
.iter()
.zip(b.attrpath.iter())
.all(|(a, b)| attr_content_eq(a, b))
&& match (a.default, b.default) {
(Some(a), Some(b)) => ir_content_eq(a, b),
(None, None) => true,
_ => false,
}
}
(Ir::If(a), Ir::If(b)) => {
ir_content_eq(a.cond, b.cond)
&& ir_content_eq(a.consq, b.consq)
&& ir_content_eq(a.alter, b.alter)
}
(Ir::Call(a), Ir::Call(b)) => {
ir_content_eq(a.func, b.func) && ir_content_eq(a.arg, b.arg)
}
(Ir::Assert(a), Ir::Assert(b)) => {
a.assertion_raw == b.assertion_raw
&& ir_content_eq(a.assertion, b.assertion)
&& ir_content_eq(a.expr, b.expr)
}
(Ir::ConcatStrings(a), Ir::ConcatStrings(b)) => {
a.force_string == b.force_string
&& a.parts.len() == b.parts.len()
&& a.parts
.iter()
.zip(b.parts.iter())
.all(|(a, b)| ir_content_eq(a, b))
}
(Ir::Path(a), Ir::Path(b)) => ir_content_eq(a.expr, b.expr),
(Ir::Func(a), Ir::Func(b)) => {
ir_content_eq(a.body, b.body)
&& ir_content_eq(a.arg, b.arg)
&& match (&a.param, &b.param) {
(Some(a), Some(b)) => param_content_eq(a, b),
(None, None) => true,
_ => false,
}
&& thunks_content_eq(&a.thunks, &b.thunks)
}
(Ir::TopLevel(a), Ir::TopLevel(b)) => {
ir_content_eq(a.body, b.body) && thunks_content_eq(&a.thunks, &b.thunks)
}
(Ir::Arg(a), Ir::Arg(b)) => a.inner == b.inner,
(Ir::Thunk(a), Ir::Thunk(b)) => a.inner == b.inner,
(Ir::Builtins(_), Ir::Builtins(_)) => true,
(Ir::Builtin(a), Ir::Builtin(b)) => a.inner == b.inner,
(Ir::CurPos(a), Ir::CurPos(b)) => a.span == b.span,
(Ir::ReplBinding(a), Ir::ReplBinding(b)) => a.inner == b.inner,
(Ir::ScopedImportBinding(a), Ir::ScopedImportBinding(b)) => a.inner == b.inner,
(Ir::With(a), Ir::With(b)) => {
ir_content_eq(a.namespace, b.namespace)
&& ir_content_eq(a.body, b.body)
&& thunks_content_eq(&a.thunks, &b.thunks)
}
(Ir::WithLookup(a), Ir::WithLookup(b)) => a.inner == b.inner,
_ => false,
}
}

View File

@@ -101,9 +101,9 @@ fn run_eval(context: &mut Context, src: ExprSource) -> Result<()> {
} else {
unreachable!()
};
match context.eval_shallow(src) {
match context.eval_deep(src) {
Ok(value) => {
println!("{value}");
println!("{}", value.display_compat());
}
Err(err) => {
eprintln!("{:?}", miette::Report::new(*err));

View File

@@ -1 +1 @@
[ { } { a = 1; } { a = 1; } { a = "a"; } { m = 1; } { m = "m"; } { n = 1; } { n = "n"; } { n = 1; p = 2; } { n = "n"; p = "p"; } { n = 1; p = 2; } { n = "n"; p = "p"; } { a = "a"; b = "b"; c = "c"; d = "d"; e = "e"; f = "f"; g = "g"; h = "h"; i = "i"; j = "j"; k = "k"; l = "l"; m = "m"; n = "n"; o = "o"; p = "p"; q = "q"; r = "r"; s = "s"; t = "t"; u = "u"; v = "v"; w = "w"; x = "x"; y = "y"; z = "z"; } true ]
[ { } { a = 1; } { a = 1; } { a = "a"; } { m = 1; } { m = "m"; } { n = 1; } { n = "n"; } { n = 1; p = 2; } { n = "n"; p = "p"; } «repeated» «repeated» { a = "a"; b = "b"; c = "c"; d = "d"; e = "e"; f = "f"; g = "g"; h = "h"; i = "i"; j = "j"; k = "k"; l = "l"; m = "m"; n = "n"; o = "o"; p = "p"; q = "q"; r = "r"; s = "s"; t = "t"; u = "u"; v = "v"; w = "w"; x = "x"; y = "y"; z = "z"; } true ]

View File

@@ -1 +1 @@
[ null <PRIMOP> <PRIMOP-APP> <LAMBDA> [ [ «repeated» ] ] ]
[ null <PRIMOP> <PRIMOP-APP> <LAMBDA> [ «repeated» ] ]

View File

@@ -6,6 +6,8 @@ trace
toString
(deepSeq "x")
(a: a)
# [ «repeated» ] instead of [ [ «repeated» ] ]
# matches Lix's behaviour
(
let
x = [ x ];

View File

@@ -1 +1 @@
[ [ 42 77 147 249 483 526 ] [ 526 483 249 147 77 42 ] [ "bar" "fnord" "foo" "xyzzy" ] [ { key = 1; value = "foo"; } { key = 1; value = "fnord"; } { key = 2; value = "bar"; } ] [ [ ] [ ] [ 1 ] [ 1 4 ] [ 1 5 ] [ 1 6 ] [ 2 ] [ 2 3 ] [ 3 ] [ 3 ] ] ]
[ [ 42 77 147 249 483 526 ] [ 526 483 249 147 77 42 ] [ "bar" "fnord" "foo" "xyzzy" ] [ { key = 1; value = "foo"; } { key = 1; value = "fnord"; } { key = 2; value = "bar"; } ] [ [ ] [ ] [ 1 ] [ 1 4 ] [ 1 5 ] [ 1 6 ] [ 2 ] [ 2 3 ] [ 3 ] «repeated» ] ]