1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
//! [`ExprBuilder`]: the emitter that turns a client-side expression into a
//! [`LogicalProgram`]. A caller emits freely; [`ExprBuilder::build`] is where it
//! is told, once, what is unsupported.
use crate::{CalendarOp, ConstIdx, ExprValidateErr, LogicalInstr, LogicalProgram, Reg, Sink, MAX_REGS};
/// Accumulates the instructions and constants of one expression program.
#[derive(Default)]
pub struct ExprBuilder {
instrs: Vec<LogicalInstr>,
const_strings: Vec<Vec<u8>>,
}
impl ExprBuilder {
pub fn new() -> Self {
Self::default()
}
/// False once the program is past the register cap, where `build` rejects it
/// whatever follows — so both dedup scans below stop paying there, and
/// lowering a huge expression stays linear rather than quadratic in its size.
fn within_reg_cap(&self) -> bool {
self.instrs.len() <= MAX_REGS
}
/// The register holding `instr`'s value: the one an identical instruction
/// already writes, else a fresh one — its own index. Every instruction is a
/// pure function of its operands, so identical ones share a register.
pub fn emit(&mut self, instr: LogicalInstr) -> Reg {
// A lift of a constant is another constant — algebraic and
// schema-independent, so it folds here rather than at resolve. The
// integer constant it reads stays: a register *is* its instruction's
// index, so `emit` can only append or reuse, never delete.
let instr = match instr {
// `get`, not an index: an out-of-range operand is `build`'s to reject.
LogicalInstr::IntToFloat { a } => match self.instrs.get(a.0 as usize) {
Some(&LogicalInstr::LoadConst { val, unsigned }) => {
return self.const_f64(if unsigned { val as u64 as f64 } else { val as f64 })
}
_ => instr,
},
// An overflowing day count stays the kernel, which makes it NULL.
LogicalInstr::Calendar {
op: CalendarOp::ToMicros,
a,
micros: false,
} => match self.instrs.get(a.0 as usize) {
Some(&LogicalInstr::LoadConst { val, unsigned: false }) => match crate::calendar::days_to_micros(val) {
(val, false) => LogicalInstr::LoadConst { val, unsigned: false },
(_, true) => instr,
},
_ => instr,
},
// A range check of a value already range-checked into the same type is
// that value: it is whole in the target and carries the same U64 tracking.
LogicalInstr::IntCast { a, fi }
if self.instrs.get(a.0 as usize).and_then(LogicalInstr::range_check) == Some(fi) =>
{
return a
}
_ => instr,
};
if self.within_reg_cap() {
if let Some(i) = self.instrs.iter().position(|e| *e == instr) {
return Reg(i as u16);
}
}
let reg = Reg(self.instrs.len() as u16);
self.instrs.push(instr);
reg
}
/// The const-pool index of `bytes`, shared with an earlier equal entry. The
/// pool is byte-transparent — german-string cells and packed i64 sets share
/// it, each read by the opcode that indexes it. Only a miss allocates.
pub fn add_const_bytes(&mut self, bytes: &[u8]) -> ConstIdx {
if self.within_reg_cap() {
if let Some(i) = self.const_strings.iter().position(|c| c.as_slice() == bytes) {
return ConstIdx(i as u32);
}
}
let idx = ConstIdx(self.const_strings.len() as u32);
self.const_strings.push(bytes.to_vec());
idx
}
/// The register holding `v`'s f64 image. A float register *is* an i64 slot
/// carrying `to_bits`, and the kernels read it back through that same codec,
/// so a caller must never write `v as i64` — which compiles and is silently
/// a different number.
pub fn const_f64(&mut self, v: f64) -> Reg {
self.emit(LogicalInstr::LoadConst {
val: crate::batch::encode_f64(v),
unsigned: false,
})
}
/// The const index of `values` as an `IntInSet` pool: strictly ascending,
/// packed `N × 8-byte LE`.
pub fn add_const_int_set(&mut self, mut values: Vec<i64>) -> ConstIdx {
values.sort_unstable();
values.dedup();
self.add_const_bytes(gnitz_wire::as_le_bytes(&values))
}
/// The typed program, held to every structural rule a schema is not needed
/// for. `sinks` is its output — one `Sink::Reg` for a filter or scalar, one
/// sink per output payload slot for a map.
pub fn build(self, sinks: Vec<Sink>) -> Result<LogicalProgram, ExprValidateErr> {
LogicalProgram::from_instrs(self.instrs, sinks, self.const_strings)
}
}
#[cfg(test)]
#[path = "tests/builder.rs"]
mod tests;