Skip to content

Commit 6db526b

Browse files
frankmcsherrycodexclaude
committed
DDIR: join fragments, a(x, y) * b(y, z), with one flatmap after the joins (spike)
A join fragment is a pipeline source made of atoms joined by `*`. Each atom names an existing binding and gives a pattern for its key and its value. Shared variables become join keys, and literals, variants and repeated variables become tests. The rows are every bound variable once, in order of first appearance, as the key, with a unit value; a following `map` builds the head. The expansion is fixed by the text, with no planner. Atoms join in written order, each against its binding's own arrangement. A later atom's key must be built from bound variables, so a test never forms a new arrangement. Key variables are read from the join's key slot, and the first atom joins directly when its key is the next key. Everything after the last join, the tests and the output layout, is one `flatmap` that returns zero or one rows. A variant's payload is read from its lane with `enlist unweave`, and literal and equality tests keep or drop the row with a list `filter`, so only the rows kept are built. Dropping dead bindings is left to the projection pass. Co-Authored-By: GPT-6 Sol (Codex) <noreply@openai.com> Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H6wB4H99TLw4tqu4x48pXL
1 parent 8b03a29 commit 6db526b

6 files changed

Lines changed: 727 additions & 4 deletions

File tree

‎interactive/src/lower.rs‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ fn resolve_expr(expr: &mut Expr, here: &[String], item: Option<&str>,
196196
resolve_expr(r, here, item, scopes, plan);
197197
}
198198
Expr::Concat(parts) => for part in parts { resolve_expr(part, here, item, scopes, plan); },
199+
Expr::Fragment(_) => unreachable!("fragments expand before path resolution"),
199200
Expr::Input(..) | Expr::Import(_) | Expr::Name(_) | Expr::ResolvedPath { .. } => {},
200201
}
201202
}
@@ -245,6 +246,7 @@ fn expr_free_names(expr: &Expr, out: &mut BTreeSet<RefName>) {
245246
Expr::Input(..) | Expr::Import(_) => {},
246247
Expr::Name(n) => { out.insert(RefName::Plain(n.clone())); },
247248
Expr::ResolvedPath { .. } => {},
249+
Expr::Fragment(_) => unreachable!("fragments expand before lowering"),
248250
Expr::Path(..) => unreachable!("paths must be resolved before lowering"),
249251
Expr::TypedSource(e, _, _) | Expr::Map(e, _) | Expr::Reduce(e, _) | Expr::Filter(e, _)
250252
| Expr::Negate(e) | Expr::LiftIter(e)
@@ -303,6 +305,7 @@ pub(crate) fn is_arranged(input: &st::Ref, items: &[st::Item]) -> bool {
303305
}
304306

305307
pub fn lower_tree(mut stmts: Vec<Stmt>) -> st::Program {
308+
crate::parse::pipe::fragment::expand(&mut stmts);
306309
let mut scopes = HashMap::new();
307310
// The program root has no source-level name; `::` is its only explicit anchor.
308311
let mut visible = HashMap::new();
@@ -381,6 +384,7 @@ impl ScopeLower {
381384
.unwrap_or_else(|| panic!("unresolved name `{}`", name)),
382385
Expr::ResolvedPath { anchor, target, field } => self.env.get(&RefName::Path(PathKey { anchor: anchor.clone(), target: target.clone(), field: field.clone() })).cloned()
383386
.unwrap_or_else(|| panic!("unresolved path `::{}::{}`", target.join("::"), field)),
387+
Expr::Fragment(_) => unreachable!("fragments expand before lowering"),
384388
Expr::Path(..) => unreachable!("paths must be resolved before lowering"),
385389
Expr::Map(e, p) => { let r = self.lower_expr(e); self.push(st::Node::Linear { input: r, ops: vec![LinearOp::Project(p.clone())] }) },
386390
Expr::Filter(e, c) => { let r = self.lower_expr(e); self.push(st::Node::Linear { input: r, ops: vec![LinearOp::Filter(c.clone())] }) },

‎interactive/src/parse/mod.rs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ pub enum Expr {
5353
Path(Vec<String>, bool),
5454
/// A path checked against the scope tree before lowering.
5555
ResolvedPath { anchor: Vec<String>, target: Vec<String>, field: String },
56+
/// A join fragment, expanded to ordinary collection syntax before IR lowering.
57+
Fragment(Box<pipe::fragment::Fragment>),
5658
Map(Box<Expr>, Lambda),
5759
Join(Box<Expr>, Box<Expr>, Lambda),
5860
/// `stream | half_join(arrangement, strict, projection)`: see `scope_ir::Node::HalfJoin`.

‎interactive/src/parse/pipe.rs‎

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,20 @@
6262
//! and each stage applies to everything to its left: `a | map(f) + b | distinct` is
6363
//! `((a | map(f)) + b) | distinct`. To add a pipeline, parenthesize it or put it first.
6464
//!
65+
//! A join fragment is a pipeline source: `a(x, y) * b(y, z) | map(((x, y, z), ()) -> (x, z))`.
66+
//! Atoms name existing bindings (including scope paths); they always have key and value arguments.
67+
//! Every later key is built from earlier variables, tuples, unit, or variant constructors.
68+
//! It cannot contain a literal, wildcard, or new variable: atoms keep their existing arrangements.
69+
//! The first key and every value accept variables, `_`, tuples, integer/list/string literals,
70+
//! and constructors with payload patterns, such as `Some(x)` or `Type::Variant(x)`.
71+
//! Reusing a variable tests equality. Tests run after all joins, except variant tags needed by
72+
//! later lookup keys. Rows contain each variable once, in first-appearance order, keyed by their
73+
//! tuple (one variable is scalar, none is unit), with unit value. Weights multiply; no distinct.
74+
//! The tests and the output layout after the last join are one `flatmap`: a variant's payload is
75+
//! read from its lane with `unweave`, and literal and equality tests keep or drop the row with a
76+
//! list `filter`, so only the rows kept are built.
77+
//! Expansion precedes IR lowering; parsed DDP retains the fragment and lowered DDP shows it expanded.
78+
//!
6579
//! Statements: `let x = …;`, `set x = …;` (a feedback variable in a named
6680
//! scope), `name: { … }` (a nested scope), `output "name" = …;` (a
6781
//! program output, root scope only). An optional `var x;` or
@@ -117,6 +131,8 @@
117131
use super::*;
118132
use crate::ir::Lambda;
119133

134+
pub mod fragment;
135+
120136
#[derive(Debug, Clone, PartialEq)]
121137
enum Token {
122138
Let, Var, Set, Output, Type, Enum,
@@ -126,7 +142,7 @@ enum Token {
126142
Lambda { pat: String, body: String },
127143
LParen, RParen, LBrace, RBrace,
128144
Comma, Semi, Colon, ColonColon, Eq,
129-
Pipe, Plus, Minus, Eof,
145+
Pipe, Plus, Minus, Star, LBracket, RBracket, Eof,
130146
}
131147

132148
/// A shape before names in type declarations have been resolved.
@@ -266,6 +282,9 @@ fn lex(input: &str) -> Vec<Token> {
266282
';' => { chars.next(); tokens.push(Token::Semi); },
267283
'|' => { chars.next(); tokens.push(Token::Pipe); },
268284
'+' => { chars.next(); tokens.push(Token::Plus); },
285+
'*' => { chars.next(); tokens.push(Token::Star); },
286+
'[' => { chars.next(); tokens.push(Token::LBracket); },
287+
']' => { chars.next(); tokens.push(Token::RBracket); },
269288
'=' => { chars.next(); tokens.push(Token::Eq); },
270289
'"' => {
271290
// String literal: the quoted name in `import "..."` / `output "..." = ...`.
@@ -546,13 +565,13 @@ impl Parser {
546565
/// An atom followed by stages, applied left to right: `| op`, `+ atom` and `- atom`. A run of
547566
/// `+`/`-` stages is one `Concat`.
548567
fn parse_pipe_expr(&mut self) -> Expr {
549-
let mut expr = self.parse_atom();
568+
let mut expr = self.parse_product();
550569
let mut concat = false;
551570
loop {
552571
let part = match self.peek() {
553572
Token::Pipe => { self.next(); expr = self.parse_pipe_op(expr); concat = false; continue },
554-
Token::Plus => { self.next(); self.parse_atom() },
555-
Token::Minus => { self.next(); Expr::Negate(Box::new(self.parse_atom())) },
573+
Token::Plus => { self.next(); self.parse_product() },
574+
Token::Minus => { self.next(); Expr::Negate(Box::new(self.parse_product())) },
556575
_ => break,
557576
};
558577
expr = match expr {
@@ -564,6 +583,11 @@ impl Parser {
564583
expr
565584
}
566585

586+
fn parse_product(&mut self) -> Expr {
587+
let source = self.parse_atom();
588+
if *self.peek() == Token::LParen { self.fragment(source) } else { source }
589+
}
590+
567591
fn parse_atom(&mut self) -> Expr {
568592
match self.peek().clone() {
569593
Token::Input => {

0 commit comments

Comments
 (0)