Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 65 additions & 45 deletions expr/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2038,56 +2038,76 @@ impl ExprEnv {
}
}

/// One past this env's own term, when a stamp says where that is; 0 when nothing does.
#[inline]
fn stamped_end(&self) -> u32 {
if self.ground_skip != 0 { self.offset + self.ground_skip as u32 } else { 0 }
}

/// Split the `k` subterms laid out consecutively at this env's focus into `dest`, threading
/// the de Bruijn base across them: each one's `v` is this env's `v` plus every introduction
/// in its preceding siblings, so all `k` resolve against a single variable namespace.
///
/// The focus is the first subterm, not a parent tag, so the run does not have to be wrapped
/// in an `Arity(k)`. That is what the sinks need: their stored path is the bare operand run
/// of a fixed-arity form with the head already stripped, so they split it in place with the
/// form's operand count instead of copying it under a synthetic arity byte just to give
/// [`args`](ExprEnv::args) something to read `k` off.
pub fn subterms(&self, k: u8, dest: &mut Vec<Self>) {
self.subterms_from(k, 0, dest)
}

/// [`subterms`](ExprEnv::subterms) with the run's end supplied, or 0 when the caller does not
/// know it. A caller that does -- `args`, whose parent tag carries a ground stamp -- lets the
/// last subterm inherit that stamp, which is the one the walk below never measures because
/// there is no following sibling to advance to.
fn subterms_from(&self, k: u8, run_end: u32, dest: &mut Vec<Self>) {
let mut env = self.clone();
for sk in 0..k {
let ne = env.clone();
dest.push(ne);
// The traversal below exists only to advance `env` past this subterm to reach the
// NEXT one, so after the last it is pure waste -- and it costs O(subterm span). On a
// right-nested pattern, where each level's last child is the whole remaining term,
// paying it at every level made a descent that calls `args` per node
// (`Space::coreferential_transition`) quadratic in the pattern's size. Skipping it
// makes such a descent linear.
if sk + 1 == k {
// The one subterm the advancement walk never measures. A stamped run measures it
// anyway: the run's end IS the last subterm's end, and a ground run has ground
// subterms.
if run_end != 0 {
dest.last_mut().unwrap().ground_skip = (run_end - env.offset) as u16;
}
break;
}
// The advancement walk visits every item regardless, so let it count the variables it
// passes: a subterm it saw none in earns a skip stamp for free -- independently of
// whether the RUN is ground, which is what lets a constant conjunct inside a
// variable-carrying conjunction reach `unify` stamped and settle against a stamped
// fact by byte comparison.
let (se, _, se_offset) = traverseh!((), (), (u8, bool), env.subsexpr(), (0u8, false),
|c: &mut (u8, bool), o| { c.0 += 1; c.1 = true; },
|c: &mut (u8, bool), o, r| { c.1 = true; },
|_, o, _| {},
|_, o, _| {},
|_, o, x, y| {},
|_, _, _| {});

if !se.1 && se_offset > 0 && se_offset <= u16::MAX as usize {
dest.last_mut().unwrap().ground_skip = se_offset as u16;
}
env.offset += se_offset as u32;
env.v += se.0;
}
}

pub fn args(&self, dest: &mut Vec<Self>) {
unsafe {
match byte_item(*self.subsexpr().ptr) {
Tag::NewVar | Tag::VarRef(_) | Tag::SymbolSize(_) => { }
Tag::Arity(k) => {
let mut env = ExprEnv{
n: self.n,
v: self.v,
offset: self.offset + 1,
ground_skip: 0,
base: self.base,
};
for sk in 0..k {
let ne = env.clone();
dest.push(ne);
// The traversal below exists only to advance `env` past this child to reach
// the NEXT one, so after the last child it is pure waste -- and it costs
// O(child span). On a right-nested pattern, where each level's last child is
// the whole remaining term, paying it at every level made a descent that
// calls `args` per node (`Space::coreferential_transition`) quadratic in the
// pattern's size. Skipping it makes such a descent linear.
if sk + 1 == k {
// The one child the advancement walk never measures. A stamped parent
// measures it anyway: the parent's end IS the last child's end, and a
// ground parent has ground children.
if self.ground_skip != 0 {
let end = self.offset + self.ground_skip as u32;
dest.last_mut().unwrap().ground_skip = (end - env.offset) as u16;
}
break;
}
// The advancement walk visits every item of the child regardless, so let it
// count the variables it passes: a child it saw none in earns a skip stamp
// for free -- independently of whether the PARENT is ground, which is what
// lets a constant conjunct inside a variable-carrying conjunction reach
// `unify` stamped and settle against a stamped fact by byte comparison.
let (se, _, se_offset) = traverseh!((), (), (u8, bool), env.subsexpr(), (0u8, false),
|c: &mut (u8, bool), o| { c.0 += 1; c.1 = true; },
|c: &mut (u8, bool), o, r| { c.1 = true; },
|_, o, _| {},
|_, o, _| {},
|_, o, x, y| {},
|_, _, _| {});

if !se.1 && se_offset > 0 && se_offset <= u16::MAX as usize {
dest.last_mut().unwrap().ground_skip = se_offset as u16;
}
env.offset += se_offset as u32;
env.v += se.0;
}
self.offset(1).subterms_from(k, self.stamped_end(), dest)
}
}
}
Expand Down
112 changes: 112 additions & 0 deletions kernel/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1248,6 +1248,114 @@ fn sink_pure_ignored_guard_addressing() {
assert_eq!(res, "(tag $a)\nignored");
}

fn sink_pure_compound_capture() {
let mut s = Space::new();

// #136: a compound capture pattern destructures the call's result, so the template
// sees the tuple's fields rather than the whole tuple. The second exec nests the
// pattern one level deeper.
const SPACE_EXPRS: &str = r#"
(exec 0 (,) (O (pure (R $x $y) ($x $y) (tuple 1 2))))
(exec 1 (,) (O (pure (S $a $b) (($a) ($b)) (tuple (tuple 3) (tuple 4)))))
"#;

s.add_all_sexpr(SPACE_EXPRS.as_bytes()).unwrap();

let mut t0 = Instant::now();
let steps = s.metta_calculus(1000000000000000);
println!("elapsed {} steps {} size {}", t0.elapsed().as_millis(), steps, s.btm.val_count());

let mut v = vec![];
s.dump_sexpr(expr!(s, "[3] R $ $"), expr!(s, "[3] R _1 _2"), &mut v);
s.dump_sexpr(expr!(s, "[3] S $ $"), expr!(s, "[3] S _1 _2"), &mut v);
let res = String::from_utf8_lossy_owned(v);

println!("result: {res}");
assert_eq!(res, "(R 1 2)\n(S 3 4)\n");
}

fn sink_pure_pattern_rejection() {
let mut s = Space::new();

// A result that does not unify with the pattern emits nothing: a ground element
// mismatch (3 vs 1), then a repeated variable against unequal fields. The third
// exec is the accepting control for the repeated-variable shape.
const SPACE_EXPRS: &str = r#"
(exec 0 (,) (O (pure (R a $y) (3 $y) (tuple 1 2))))
(exec 1 (,) (O (pure (R b $x) ($x $x) (tuple 1 2))))
(exec 2 (,) (O (pure (R c $x) ($x $x) (tuple 7 7))))
"#;

s.add_all_sexpr(SPACE_EXPRS.as_bytes()).unwrap();

let mut t0 = Instant::now();
let steps = s.metta_calculus(1000000000000000);
println!("elapsed {} steps {} size {}", t0.elapsed().as_millis(), steps, s.btm.val_count());

let mut v = vec![];
s.dump_sexpr(expr!(s, "[3] R $ $"), expr!(s, "[3] R _1 _2"), &mut v);
let res = String::from_utf8_lossy_owned(v);

println!("result: {res}");
assert_eq!(res, "(R c 7)\n");
}

fn sink_pure_symbol_guard() {
let mut s = Space::new();

// A ground symbol pattern is a pure guard: only the body match whose call result
// equals the symbol emits. reverse_symbol(123) is 321; reverse_symbol(456) is not.
const SPACE_EXPRS: &str = r#"
(A 123)
(A 456)

(exec 0 (, (A $i))
(O (pure (ok $i) 321 (reverse_symbol $i))))
"#;

s.add_all_sexpr(SPACE_EXPRS.as_bytes()).unwrap();

let mut t0 = Instant::now();
let steps = s.metta_calculus(1000000000000000);
println!("elapsed {} steps {} size {}", t0.elapsed().as_millis(), steps, s.btm.val_count());

let mut v = vec![];
s.dump_sexpr(expr!(s, "[2] ok $"), expr!(s, "[2] ok _1"), &mut v);
let res = String::from_utf8_lossy_owned(v);

println!("result: {res}");
assert_eq!(res, "(ok 123)\n");
}

fn sink_pure_compound_multiplicity() {
let mut s = Space::new();

// One capture per body match: the instantiated pattern carries the match's own
// ground value next to the capture variable, so each match destructures its own
// result and the ground position is checked per firing.
const SPACE_EXPRS: &str = r#"
(N 1)
(N 2)
(N 3)

(exec 0 (, (N $n))
(O (pure (P $n $a) ($a $n) (tuple 5 $n))))
"#;

s.add_all_sexpr(SPACE_EXPRS.as_bytes()).unwrap();

let mut t0 = Instant::now();
let steps = s.metta_calculus(1000000000000000);
println!("elapsed {} steps {} size {}", t0.elapsed().as_millis(), steps, s.btm.val_count());

let mut v = vec![];
s.dump_sexpr(expr!(s, "[3] P $ $"), expr!(s, "[3] P _1 _2"), &mut v);
let res = String::from_utf8_lossy_owned(v);

println!("result: {res}");
assert_eq!(res, "(P 1 5)\n(P 2 5)\n(P 3 5)\n");
}

fn sink_bass64url_ident() {
let mut s = Space::new();

Expand Down Expand Up @@ -6329,6 +6437,10 @@ fn main() {
sink_pure_quote_collapse_symbol();
sink_pure_explode_collapse_ident();
sink_pure_ignored_guard_addressing();
sink_pure_compound_capture();
sink_pure_pattern_rejection();
sink_pure_symbol_guard();
sink_pure_compound_multiplicity();
sink_bass64url_ident();
sink_hex_ident();
sink_hash_expr();
Expand Down
83 changes: 64 additions & 19 deletions kernel/src/sinks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1132,30 +1132,75 @@ impl Sink for PureSink {
let prz_ptr = (&prz) as *const OneFactor<_>;
let mut changed = false;
let mut buffer: Vec<u8> = Vec::with_capacity(1 << 32);
let mut wargs: Vec<ExprEnv> = Vec::new();
let mut pbuffer: Vec<u8> = Vec::new();
let mut pstack = Vec::new();
let mut passignments = Vec::new();
crate::space::Space::query_multi_raw(unsafe { prz_ptr.cast_mut().as_mut().unwrap() }, &[ExprEnv::new(0, Expr{ ptr: v.as_ptr().cast_mut() })], |refs_bindings, loc| {

for b in prz.child_mask().and(&ByteMask(crate::space::SIZES)).iter() {
let Tag::SymbolSize(size) = byte_item(b) else { unreachable!() };
prz.descend_to_byte(b);
debug_assert!(prz.path_exists());
if !prz.descend_first_k_path(size as _) { unreachable!() }
loop {
let clen = prz.origin_path().len();

// A pattern led by a symbol or a compound is a rejection pattern (#136): evaluate
// the call, unify the result against the pattern, and emit the template under the
// resulting bindings; a result that does not unify emits nothing. The engine's own
// unify/apply do the matching, so data-side variables and occurs rejection behave
// exactly as they do in a rewrite over `(mytag <pat>)`.
for class in [crate::space::SIZES, crate::space::ARITIES] {
for b in prz.child_mask().and(&ByteMask(class)).iter() {
prz.descend_to_byte(b);
debug_assert!(prz.path_exists());
let mut rz = prz.fork_read_zipper();
'vals: while rz.to_next_val() {
let p = rz.origin_path();
trace!(target: "sink", "path number {:?}", serialize(&p[clen..]));
todo!();
}
'triples: while rz.to_next_val() {
// The value path is the concatenated (template pattern call) triple, the
// request having stripped `(pure`. `subterms` splits the run in place and
// threads the de Bruijn base across the three, so they share one variable
// namespace and the pattern's VarRefs resolve to the template's
// introductions.
let full = rz.origin_path();
wargs.clear();
ExprEnv::new(0, Expr { ptr: full.as_ptr().cast_mut() }).subterms(3, &mut wargs);
let &[tpl_env, pat_env, call_env] = &wargs[..] else {
trace!(target: "sink", "pure malformed triple {}", serialize(full));
continue 'triples
};

if !prz.to_next_k_path(size as _) { break }
let mut res = match self.scope.eval(ExprSource::new(call_env.subsexpr().ptr)) {
Ok(res) => { res }
Err(er) => { trace!(target: "pure", "err {}", er); continue 'triples }
};
trace!(target: "sink", "pattern guard result {:?}", serialize(&res[..]));

let mut pairs = vec![(pat_env, ExprEnv::new(1, Expr { ptr: res.as_mut_ptr() }))];
match unify(&mut pairs) {
Ok(bindings) => {
pbuffer.clear();
// `expr-opt` moved the macro onto `ItemSink`; the buffer goes in
// through `VecSink` the way `Space`'s emit paths do.
let applied = {
let mut sink = mork_expr::VecSink(&mut pbuffer);
mork_expr::apply_e_clears_stacks_and_cycles_check!(0, 0, 0, tpl_env.subsexpr(), &bindings, sink, pstack, passignments)
};
if let (_, _, true) = applied {
let rooted = wz.root_prefix_path().len();
if pbuffer.len() > rooted {
trace!(target: "sink", "pattern guard emit '{}'", serialize(&pbuffer[..]));
wz.move_to_path(&pbuffer[rooted..]);
wz.set_val(());
changed |= true;
} else {
// A fully constant template makes the write request root
// cover the whole sink expression; nothing can be emitted
// below it. Same limitation as the variable arms.
trace!(target: "sink", "pure template within its request root, skipping");
}
}
}
Err(f) => {
trace!(target: "sink", "pattern guard rejected {:?}", f);
}
}
self.scope.return_alloc(res);
}
if !prz.ascend_byte() { unreachable!() }
}
if !prz.ascend_byte() { unreachable!() }
}

for b in prz.child_mask().and(&ByteMask(crate::space::ARITIES)).iter() {
todo!();
}

if prz.descend_to_existing_byte(item_byte(Tag::NewVar)) {
Expand Down