use crate::calendar::CalendarOp;
use crate::like::{LikeMatcher, LikePattern};
use crate::{ColumnLocator, SchemaFacts};
use gnitz_wire::{decode_all, encode_german_string, FixedInt, ScalarKind, TypeCode, Writer};
use std::fmt;
pub(crate) const MAX_REGS: usize = u64::BITS as usize;
const MAX_CONST_POOL: usize = MAX_REGS;
const INSTR_WORDS: usize = 5;
const SINK_WORDS: usize = 2;
const INSTR_BYTES: usize = INSTR_WORDS * 4;
const SINK_BYTES: usize = SINK_WORDS * 4;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ExprValidateErr {
UnknownOpcode(u32),
TooManyRegs(u32),
RegOutOfRange {
reg: u16,
num_regs: u32,
},
RegReadBeforeWrite {
reg: u16,
},
RegClassMismatch {
reg: u16,
},
ConstIdxOutOfRange {
const_idx: u32,
n: usize,
},
PoolEntryMalformed {
const_idx: u32,
want: PoolEntry,
},
ColOutOfRange {
col: u32,
num_columns: usize,
},
ColNotPayload {
col: u32,
},
ColKindMismatch {
col: u32,
type_code: TypeCode,
want: &'static str,
},
CopyTypeMismatch {
col: u32,
src_tc: TypeCode,
out: u32,
out_tc: TypeCode,
},
EmitSlotWidth {
out: u32,
type_code: TypeCode,
},
EmitClassMismatch {
out: u32,
type_code: TypeCode,
},
OutputSlotCountMismatch {
sinks: usize,
num_payload_cols: usize,
},
OutputRoleMismatch,
CorruptBlob(String),
BadSelector {
op: u32,
selector: u32,
},
BadSinkKind(u32),
BadWalk(String),
}
impl fmt::Display for ExprValidateErr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ExprValidateErr::TooManyRegs(n) => {
write!(
f,
"expression needs {n} registers; the limit is {MAX_REGS} — \
split the predicate, or project fewer computed columns"
)
}
ExprValidateErr::ColKindMismatch { col, type_code, want } => {
write!(
f,
"column {col} (type code {type_code}) cannot be used here; this operator needs {want}"
)
}
ExprValidateErr::ColNotPayload { col } => {
write!(
f,
"column {col} is part of the primary key; this operator needs a payload column"
)
}
ExprValidateErr::CorruptBlob(msg) | ExprValidateErr::BadWalk(msg) => write!(f, "{msg}"),
other => write!(f, "{other:?}"),
}
}
}
const SCALAR_COL: &str = "a fixed-width integer or floating-point column";
type ColTypeTest = (fn(TypeCode) -> bool, &'static str);
#[derive(Clone, Copy)]
enum ColKind {
AnyCol,
ScalarCol,
StringPayload,
}
impl ColKind {
fn type_test(self) -> Option<ColTypeTest> {
match self {
Self::AnyCol => None,
Self::ScalarCol => Some((|t| ScalarKind::from_type_code(t).is_some(), SCALAR_COL)),
Self::StringPayload => Some((TypeCode::is_german_string, "a string or blob column")),
}
}
fn payload_only(self) -> bool {
matches!(self, Self::StringPayload)
}
}
gnitz_wire::wire_enum! {
pub(crate) enum ExprOp: u32 {
LoadCol = 1,
LoadConst = 2,
IntArith = 3,
FloatArith = 4,
Cmp = 5,
FCmp = 6,
IntToFloat = 7,
FloatUnary = 8,
IntUnary = 9,
FloatToInt = 10,
IntCast = 11,
FloatToF32 = 12,
IntMinMax2 = 13,
FloatMinMax2 = 14,
Select = 15,
LoadNull = 16,
BoolBinary = 17,
BoolNot = 18,
IsNull = 19,
IsNullReg = 20,
StrColConst = 21,
StrColCol = 22,
IntInSet = 23,
LoadColStr = 24,
LoadConstStr = 25,
LoadNullStr = 26,
StrSelect = 27,
StrCmp = 28,
StrLen = 29,
StrCase = 30,
StrSubstr = 31,
StrTrim = 32,
StrLike = 33,
StrConcat = 34,
IntToStr = 35,
FloatToStr = 36,
StrToInt = 37,
StrToFloat = 38,
StrSide = 39,
StrPos = 40,
StrReverse = 41,
StrReplace = 42,
StrPad = 43,
StrSplitPart = 44,
Calendar = 45,
}
}
gnitz_wire::wire_enum! {
pub(crate) enum SinkKind: u32 {
Col = 0,
Reg = 1,
}
}
#[inline]
const fn encode_load_const(v: i64) -> (u32, u32) {
(v as u32, (v >> 32) as u32)
}
#[inline]
const fn decode_load_const(a1: u32, a2: u32) -> i64 {
((a2 as i64) << 32) | (a1 as i64 & 0xFFFF_FFFF)
}
gnitz_wire::wire_enum! {
pub enum CmpOp: u32 {
Eq = 0,
Ne = 1,
Gt = 2,
Ge = 3,
Lt = 4,
Le = 5,
}
}
impl CmpOp {
pub fn converse(self) -> CmpOp {
match self {
CmpOp::Gt => CmpOp::Lt,
CmpOp::Lt => CmpOp::Gt,
CmpOp::Ge => CmpOp::Le,
CmpOp::Le => CmpOp::Ge,
CmpOp::Eq | CmpOp::Ne => self,
}
}
}
gnitz_wire::wire_enum! {
pub enum FloatUnaryOp: u32 {
Neg = 0,
Abs = 1,
Floor = 2,
Ceil = 3,
Round = 4,
Trunc = 5,
Sqrt = 6,
Ln = 7,
Log10 = 8,
Exp = 9,
Sign = 10,
}
}
gnitz_wire::wire_enum! {
pub enum IntUnaryOp: u32 {
Neg = 0,
Abs = 1,
Sign = 2,
}
}
gnitz_wire::wire_enum! {
pub enum IntArithOp: u32 {
Add = 0,
Sub = 1,
Mul = 2,
Div = 3,
Mod = 4,
}
}
gnitz_wire::wire_enum! {
pub enum FloatArithOp: u32 {
Add = 0,
Sub = 1,
Mul = 2,
Div = 3,
Pow = 4,
}
}
gnitz_wire::wire_enum! {
pub enum TrimMode: u32 {
Both = 0,
Leading = 1,
Trailing = 2,
}
}
impl TrimMode {
#[inline]
pub(crate) fn trims_start(self) -> bool {
matches!(self, TrimMode::Both | TrimMode::Leading)
}
#[inline]
pub(crate) fn trims_end(self) -> bool {
matches!(self, TrimMode::Both | TrimMode::Trailing)
}
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub struct Reg(pub u16);
impl Reg {
fn written_before(self, i: usize) -> bool {
(self.0 as usize) < i
}
fn from_wire(w: u32) -> Self {
const { assert!(MAX_REGS < u16::MAX as usize) };
Reg(u16::try_from(w).unwrap_or(u16::MAX))
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct ConstIdx(pub u32);
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Sink {
Col(u32),
Reg(Reg),
}
impl Sink {
fn reg(self) -> Option<Reg> {
match self {
Sink::Col(_) => None,
Sink::Reg(r) => Some(r),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum LogicalInstr {
LoadCol {
col: u32,
},
LoadConst {
val: i64,
unsigned: bool,
},
IntArith {
op: IntArithOp,
a: Reg,
b: Reg,
},
FloatArith {
op: FloatArithOp,
a: Reg,
b: Reg,
},
Cmp {
op: CmpOp,
a: Reg,
b: Reg,
},
FCmp {
op: CmpOp,
a: Reg,
b: Reg,
},
IntToFloat {
a: Reg,
},
FloatUnary {
op: FloatUnaryOp,
a: Reg,
},
IntUnary {
op: IntUnaryOp,
a: Reg,
},
Calendar {
op: CalendarOp,
a: Reg,
micros: bool,
},
FloatToInt {
a: Reg,
fi: FixedInt,
},
IntCast {
a: Reg,
fi: FixedInt,
},
FloatToF32 {
a: Reg,
},
IntMinMax2 {
a: Reg,
b: Reg,
is_max: bool,
},
FloatMinMax2 {
a: Reg,
b: Reg,
is_max: bool,
},
Select {
cond: Reg,
a: Reg,
b: Reg,
},
LoadNull,
BoolBinary {
a: Reg,
b: Reg,
is_or: bool,
},
BoolNot {
a: Reg,
},
IsNull {
col: u32,
invert: bool,
},
IsNullReg {
a: Reg,
invert: bool,
},
StrColConst {
op: CmpOp,
col: u32,
const_idx: ConstIdx,
},
StrColCol {
op: CmpOp,
col_a: u32,
col_b: u32,
},
IntInSet {
value_reg: Reg,
set_idx: ConstIdx,
},
LoadColStr {
col: u32,
},
LoadConstStr {
const_idx: ConstIdx,
},
LoadNullStr,
StrSelect {
cond: Reg,
a: Reg,
b: Reg,
},
StrCmp {
op: CmpOp,
a: Reg,
b: Reg,
},
StrLen {
a: Reg,
chars: bool,
},
StrCase {
a: Reg,
upper: bool,
},
StrSubstr {
src: Reg,
start_reg: Reg,
len_reg: Option<Reg>,
},
StrTrim {
a: Reg,
mode: TrimMode,
set_idx: ConstIdx,
},
StrLike {
src: Reg,
pat_idx: ConstIdx,
ci: bool,
},
StrConcat {
a: Reg,
b: Reg,
skip_null: bool,
},
IntToStr {
a: Reg,
},
FloatToStr {
a: Reg,
},
StrToInt {
a: Reg,
fi: FixedInt,
},
StrToFloat {
a: Reg,
},
StrSide {
src: Reg,
n_reg: Reg,
left: bool,
},
StrPos {
hay: Reg,
needle: Reg,
},
StrReverse {
a: Reg,
},
StrReplace {
s: Reg,
from: Reg,
to: Reg,
},
StrPad {
s: Reg,
n_reg: Reg,
fill: Reg,
left: bool,
},
StrSplitPart {
s: Reg,
delim: Reg,
n_reg: Reg,
},
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum IntOrder {
Signed,
Unsigned,
UnsignedSigned,
}
#[derive(Clone, Copy, Debug)]
pub(crate) enum Instr {
LoadPayloadInt {
pi: u8,
fi: FixedInt,
},
LoadPayloadF32 {
pi: u8,
},
LoadPk {
off: u8,
fi: FixedInt,
},
IntArith {
op: IntArithOp,
a: u16,
b: u16,
signed: bool,
},
Cmp {
op: CmpOp,
a: u16,
b: u16,
order: IntOrder,
},
FCmp {
op: CmpOp,
a: u16,
b: u16,
},
FloatArith {
op: FloatArithOp,
a: u16,
b: u16,
},
IntToFloat {
a: u16,
signed: bool,
},
FloatUnary {
op: FloatUnaryOp,
a: u16,
},
IntUnary {
op: IntUnaryOp,
a: u16,
signed: bool,
},
Calendar {
op: CalendarOp,
a: u16,
micros: bool,
},
FloatToInt {
a: u16,
fi: FixedInt,
},
IntCast {
a: u16,
fi: FixedInt,
src_signed: bool,
},
FloatToF32 {
a: u16,
},
IntMinMax2 {
a: u16,
b: u16,
is_max: bool,
signed: bool,
},
FloatMinMax2 {
a: u16,
b: u16,
is_max: bool,
},
Select {
cond: u16,
a: u16,
b: u16,
},
LoadNull,
BoolBinary {
a: u16,
b: u16,
is_or: bool,
},
BoolNot {
a: u16,
},
IsNull {
pi: u8,
invert: bool,
},
IsNullReg {
a: u16,
invert: bool,
},
StrColConst {
op: CmpOp,
pi: u8,
const_idx: u32,
},
StrColCol {
op: CmpOp,
pi_a: u8,
pi_b: u8,
},
IntInSet {
value_reg: u16,
set_idx: u32,
},
LoadColStr {
pi: u8,
},
LoadNullStr,
StrSelect {
cond: u16,
a: u16,
b: u16,
},
StrCmp {
op: CmpOp,
a: u16,
b: u16,
},
StrLen {
a: u16,
chars: bool,
},
StrCase {
a: u16,
upper: bool,
},
StrSubstr {
src: u16,
start: IntReg,
len: Option<IntReg>,
},
StrTrim {
a: u16,
mode: TrimMode,
set_idx: u32,
},
StrLike {
src: u16,
matcher_idx: u32,
},
StrConcat {
a: u16,
b: u16,
skip_null: bool,
},
IntToStr {
a: u16,
signed: bool,
},
FloatToStr {
a: u16,
},
StrToInt {
a: u16,
fi: FixedInt,
},
StrToFloat {
a: u16,
},
StrSide {
src: u16,
n: IntReg,
left: bool,
},
StrPos {
hay: u16,
needle: u16,
},
StrReverse {
a: u16,
},
StrReplace {
s: u16,
from: u16,
to: u16,
},
StrPad {
s: u16,
n: IntReg,
fill: u16,
left: bool,
},
StrSplitPart {
s: u16,
delim: u16,
n: IntReg,
},
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct IntReg {
pub(crate) reg: u16,
pub(crate) signed: bool,
}
impl LogicalInstr {
pub(crate) fn range_check(&self) -> Option<FixedInt> {
match *self {
LogicalInstr::IntCast { fi, .. }
| LogicalInstr::FloatToInt { fi, .. }
| LogicalInstr::StrToInt { fi, .. } => Some(fi),
_ => None,
}
}
pub(crate) fn to_wire(self) -> [u32; INSTR_WORDS] {
use LogicalInstr as L;
let bin = |op: ExprOp, sel: u32, a: Reg, b: Reg| [op.as_wire(), sel, a.0 as u32, b.0 as u32, 0];
let un = |op: ExprOp, sel: u32, a: Reg| [op.as_wire(), sel, a.0 as u32, 0, 0];
let col = |op: ExprOp, sel: u32, c: u32| [op.as_wire(), sel, c, 0, 0];
match self {
L::LoadCol { col: c } => col(ExprOp::LoadCol, 0, c),
L::LoadConst { val, unsigned } => {
let (lo, hi) = encode_load_const(val);
[ExprOp::LoadConst.as_wire(), unsigned as u32, lo, hi, 0]
}
L::IntArith { op, a, b } => bin(ExprOp::IntArith, op.as_wire(), a, b),
L::FloatArith { op, a, b } => bin(ExprOp::FloatArith, op.as_wire(), a, b),
L::Cmp { op, a, b } => bin(ExprOp::Cmp, op.as_wire(), a, b),
L::FCmp { op, a, b } => bin(ExprOp::FCmp, op.as_wire(), a, b),
L::IntToFloat { a } => un(ExprOp::IntToFloat, 0, a),
L::FloatUnary { op, a } => un(ExprOp::FloatUnary, op.as_wire(), a),
L::IntUnary { op, a } => un(ExprOp::IntUnary, op.as_wire(), a),
L::Calendar { op, a, micros } => [ExprOp::Calendar.as_wire(), op.as_wire(), a.0 as u32, micros as u32, 0],
L::FloatToInt { a, fi } => un(ExprOp::FloatToInt, fi.type_code().as_wire() as u32, a),
L::IntCast { a, fi } => un(ExprOp::IntCast, fi.type_code().as_wire() as u32, a),
L::FloatToF32 { a } => un(ExprOp::FloatToF32, 0, a),
L::IntMinMax2 { a, b, is_max } => bin(ExprOp::IntMinMax2, is_max as u32, a, b),
L::FloatMinMax2 { a, b, is_max } => bin(ExprOp::FloatMinMax2, is_max as u32, a, b),
L::Select { cond, a, b } => [ExprOp::Select.as_wire(), 0, cond.0 as u32, a.0 as u32, b.0 as u32],
L::LoadNull => [ExprOp::LoadNull.as_wire(), 0, 0, 0, 0],
L::BoolBinary { a, b, is_or } => bin(ExprOp::BoolBinary, is_or as u32, a, b),
L::BoolNot { a } => un(ExprOp::BoolNot, 0, a),
L::IsNull { col: c, invert } => col(ExprOp::IsNull, invert as u32, c),
L::IsNullReg { a, invert } => un(ExprOp::IsNullReg, invert as u32, a),
L::StrColConst { op, col: c, const_idx } => {
[ExprOp::StrColConst.as_wire(), op.as_wire(), c, const_idx.0, 0]
}
L::StrColCol { op, col_a, col_b } => [ExprOp::StrColCol.as_wire(), op.as_wire(), col_a, col_b, 0],
L::IntInSet { value_reg, set_idx } => [ExprOp::IntInSet.as_wire(), 0, value_reg.0 as u32, set_idx.0, 0],
L::LoadColStr { col: c } => col(ExprOp::LoadColStr, 0, c),
L::LoadConstStr { const_idx } => col(ExprOp::LoadConstStr, 0, const_idx.0),
L::LoadNullStr => [ExprOp::LoadNullStr.as_wire(), 0, 0, 0, 0],
L::StrSelect { cond, a, b } => [ExprOp::StrSelect.as_wire(), 0, cond.0 as u32, a.0 as u32, b.0 as u32],
L::StrCmp { op, a, b } => bin(ExprOp::StrCmp, op.as_wire(), a, b),
L::StrLen { a, chars } => un(ExprOp::StrLen, chars as u32, a),
L::StrCase { a, upper } => un(ExprOp::StrCase, upper as u32, a),
L::StrSubstr { src, start_reg, len_reg } => [
ExprOp::StrSubstr.as_wire(),
0,
src.0 as u32,
start_reg.0 as u32,
len_reg.map_or(u32::MAX, |r| r.0 as u32),
],
L::StrTrim { a, mode, set_idx } => [ExprOp::StrTrim.as_wire(), mode.as_wire(), a.0 as u32, set_idx.0, 0],
L::StrLike { src, pat_idx, ci } => [ExprOp::StrLike.as_wire(), ci as u32, src.0 as u32, pat_idx.0, 0],
L::StrConcat { a, b, skip_null } => bin(ExprOp::StrConcat, skip_null as u32, a, b),
L::IntToStr { a } => un(ExprOp::IntToStr, 0, a),
L::FloatToStr { a } => un(ExprOp::FloatToStr, 0, a),
L::StrToInt { a, fi } => un(ExprOp::StrToInt, fi.type_code().as_wire() as u32, a),
L::StrToFloat { a } => un(ExprOp::StrToFloat, 0, a),
L::StrSide { src, n_reg, left } => bin(ExprOp::StrSide, left as u32, src, n_reg),
L::StrPos { hay, needle } => bin(ExprOp::StrPos, 0, hay, needle),
L::StrReverse { a } => un(ExprOp::StrReverse, 0, a),
L::StrReplace { s, from, to } => [ExprOp::StrReplace.as_wire(), 0, s.0 as u32, from.0 as u32, to.0 as u32],
L::StrPad { s, n_reg, fill, left } => [
ExprOp::StrPad.as_wire(),
left as u32,
s.0 as u32,
n_reg.0 as u32,
fill.0 as u32,
],
L::StrSplitPart { s, delim, n_reg } => [
ExprOp::StrSplitPart.as_wire(),
0,
s.0 as u32,
delim.0 as u32,
n_reg.0 as u32,
],
}
}
}
impl Sink {
fn to_wire(self) -> [u32; SINK_WORDS] {
match self {
Sink::Col(src_col) => [SinkKind::Col.as_wire(), src_col],
Sink::Reg(r) => [SinkKind::Reg.as_wire(), r.0 as u32],
}
}
}
pub(crate) fn encode_expr_blob(
code: impl ExactSizeIterator<Item = [u32; INSTR_WORDS]>,
sinks: impl ExactSizeIterator<Item = [u32; SINK_WORDS]>,
const_strings: &[Vec<u8>],
) -> Vec<u8> {
let (n, m) = (code.len(), sinks.len());
let mut w = Writer::with_capacity(
12 + n * INSTR_BYTES + m * SINK_BYTES + const_strings.iter().map(|s| 4 + s.len()).sum::<usize>(),
);
w.u32(n as u32);
for instr in code {
for word in instr {
w.u32(word);
}
}
w.u32(m as u32);
for sink in sinks {
for word in sink {
w.u32(word);
}
}
w.u32(const_strings.len() as u32);
for s in const_strings {
w.bytes32(s);
}
w.into_vec()
}
#[derive(Debug)]
pub struct LogicalProgram {
instrs: Vec<LogicalInstr>,
pub(crate) sinks: Vec<Sink>,
const_strings: Vec<Vec<u8>>,
pub(crate) str_class: u64,
}
impl LogicalProgram {
pub fn new(instrs: Vec<LogicalInstr>, sinks: Vec<Sink>, const_strings: Vec<Vec<u8>>) -> Self {
Self::from_instrs(instrs, sinks, const_strings)
.unwrap_or_else(|e| panic!("compiler-built LogicalProgram is invalid: {e:?}"))
}
pub(crate) fn from_instrs(
instrs: Vec<LogicalInstr>,
sinks: Vec<Sink>,
const_strings: Vec<Vec<u8>>,
) -> Result<Self, ExprValidateErr> {
use ExprValidateErr as E;
let mut str_class = 0u64;
if instrs.len() > MAX_REGS {
return Err(E::TooManyRegs(instrs.len() as u32));
}
for (i, instr) in instrs.iter().enumerate() {
let ops = operands(instr);
if let Some((idx, want)) = ops.pool {
check_pool_entry(idx, want, &const_strings)?;
}
for &(reg, read) in ops.reads.iter().flatten() {
if !reg.written_before(i) {
return Err(E::RegReadBeforeWrite { reg: reg.0 });
}
if read != ReadAs::NullBit && ((str_class >> reg.0) & 1 != 0) != read.wants_str() {
return Err(E::RegClassMismatch { reg: reg.0 });
}
}
if ops.write == WriteAs::Str {
str_class |= 1u64 << i;
}
}
for reg in sinks.iter().filter_map(|s| s.reg()) {
if !reg.written_before(instrs.len()) {
return Err(E::RegOutOfRange {
reg: reg.0,
num_regs: instrs.len() as u32,
});
}
}
Ok(LogicalProgram { instrs, sinks, const_strings, str_class })
}
pub fn copy_cols(copies: &[u32]) -> Self {
let sinks = copies.iter().map(|&src_col| Sink::Col(src_col)).collect();
LogicalProgram::new(Vec::new(), sinks, Vec::new())
}
pub fn instrs(&self) -> &[LogicalInstr] {
&self.instrs
}
pub fn const_strings(&self) -> &[Vec<u8>] {
&self.const_strings
}
pub fn to_blob_bytes(&self) -> Vec<u8> {
encode_expr_blob(
self.instrs.iter().copied().map(LogicalInstr::to_wire),
self.sinks.iter().copied().map(Sink::to_wire),
&self.const_strings,
)
}
pub fn from_blob(blob: &[u8]) -> Result<Self, ExprValidateErr> {
let (code, sinks, const_strings) = decode_all(blob, "expr blob", |r| {
let n = r.u32()? as usize;
let code = r.take(n * INSTR_BYTES)?;
let m = r.u32()? as usize;
let sinks = r.take(m * SINK_BYTES)?;
if m > gnitz_wire::MAX_COLUMNS {
return Err(format!("sink count {m} exceeds {}", gnitz_wire::MAX_COLUMNS));
}
let s = r.u32()? as usize;
if s > MAX_CONST_POOL {
return Err(format!("declared const-pool count {s} exceeds {MAX_CONST_POOL}"));
}
let mut const_strings: Vec<Vec<u8>> = Vec::new();
for _ in 0..s {
const_strings.push(r.bytes32()?.to_vec());
}
Ok((code, sinks, const_strings))
})
.map_err(ExprValidateErr::CorruptBlob)?;
let n = code.len() / INSTR_BYTES;
if n > MAX_REGS {
return Err(ExprValidateErr::TooManyRegs(n as u32));
}
let instrs = code
.as_chunks::<INSTR_BYTES>()
.0
.iter()
.map(Self::decode_instr)
.collect::<Result<Vec<_>, _>>()?;
let sinks = sinks
.as_chunks::<SINK_BYTES>()
.0
.iter()
.map(Self::decode_sink)
.collect::<Result<Vec<_>, _>>()?;
Self::from_instrs(instrs, sinks, const_strings)
}
fn decode_sink(p: &[u8; SINK_BYTES]) -> Result<Sink, ExprValidateErr> {
let (kind, value) = (gnitz_wire::read_u32_le(p, 0), gnitz_wire::read_u32_le(p, 4));
match SinkKind::from_wire(kind).ok_or(ExprValidateErr::BadSinkKind(kind))? {
SinkKind::Col => Ok(Sink::Col(value)),
SinkKind::Reg => Ok(Sink::Reg(Reg::from_wire(value))),
}
}
pub(crate) fn decode_instr(t: &[u8; INSTR_BYTES]) -> Result<LogicalInstr, ExprValidateErr> {
use LogicalInstr as L;
let w = |i: usize| gnitz_wire::read_u32_le(t, i * 4);
let op = ExprOp::from_wire(w(0)).ok_or(ExprValidateErr::UnknownOpcode(w(0)))?;
let (opw, sel) = (w(0), w(1));
let (a, b, c) = (Reg::from_wire(w(2)), Reg::from_wire(w(3)), Reg::from_wire(w(4)));
let bad_sel = || ExprValidateErr::BadSelector { op: opw, selector: sel };
let no_sel = |i: L| if sel == 0 { Ok(i) } else { Err(bad_sel()) };
let cmp_op = || CmpOp::from_wire(sel).ok_or_else(bad_sel);
Ok(match op {
ExprOp::LoadCol => no_sel(L::LoadCol { col: w(2) })?,
ExprOp::LoadConst => L::LoadConst {
val: decode_load_const(w(2), w(3)),
unsigned: flag(opw, sel)?,
},
ExprOp::IntArith => L::IntArith {
op: IntArithOp::from_wire(sel).ok_or_else(bad_sel)?,
a,
b,
},
ExprOp::FloatArith => L::FloatArith {
op: FloatArithOp::from_wire(sel).ok_or_else(bad_sel)?,
a,
b,
},
ExprOp::Cmp => L::Cmp { op: cmp_op()?, a, b },
ExprOp::FCmp => L::FCmp { op: cmp_op()?, a, b },
ExprOp::IntToFloat => no_sel(L::IntToFloat { a })?,
ExprOp::FloatUnary => L::FloatUnary {
op: FloatUnaryOp::from_wire(sel).ok_or_else(bad_sel)?,
a,
},
ExprOp::IntUnary => L::IntUnary {
op: IntUnaryOp::from_wire(sel).ok_or_else(bad_sel)?,
a,
},
ExprOp::Calendar => L::Calendar {
op: CalendarOp::from_wire(sel).ok_or_else(bad_sel)?,
a,
micros: flag(opw, w(3))?,
},
ExprOp::FloatToInt => L::FloatToInt { a, fi: cast_target(opw, sel)? },
ExprOp::IntCast => L::IntCast { a, fi: cast_target(opw, sel)? },
ExprOp::FloatToF32 => no_sel(L::FloatToF32 { a })?,
ExprOp::IntMinMax2 => L::IntMinMax2 { a, b, is_max: flag(opw, sel)? },
ExprOp::FloatMinMax2 => L::FloatMinMax2 { a, b, is_max: flag(opw, sel)? },
ExprOp::Select => no_sel(L::Select { cond: a, a: b, b: c })?,
ExprOp::LoadNull => no_sel(L::LoadNull)?,
ExprOp::BoolBinary => L::BoolBinary { a, b, is_or: flag(opw, sel)? },
ExprOp::BoolNot => no_sel(L::BoolNot { a })?,
ExprOp::IsNull => L::IsNull { col: w(2), invert: flag(opw, sel)? },
ExprOp::IsNullReg => L::IsNullReg { a, invert: flag(opw, sel)? },
ExprOp::StrColConst => L::StrColConst {
op: cmp_op()?,
col: w(2),
const_idx: ConstIdx(w(3)),
},
ExprOp::StrColCol => L::StrColCol { op: cmp_op()?, col_a: w(2), col_b: w(3) },
ExprOp::IntInSet => no_sel(L::IntInSet { value_reg: a, set_idx: ConstIdx(w(3)) })?,
ExprOp::LoadColStr => no_sel(L::LoadColStr { col: w(2) })?,
ExprOp::LoadConstStr => no_sel(L::LoadConstStr { const_idx: ConstIdx(w(2)) })?,
ExprOp::LoadNullStr => no_sel(L::LoadNullStr)?,
ExprOp::StrSelect => no_sel(L::StrSelect { cond: a, a: b, b: c })?,
ExprOp::StrCmp => L::StrCmp { op: cmp_op()?, a, b },
ExprOp::StrLen => L::StrLen { a, chars: flag(opw, sel)? },
ExprOp::StrCase => L::StrCase { a, upper: flag(opw, sel)? },
ExprOp::StrSubstr => no_sel(L::StrSubstr {
src: a,
start_reg: b,
len_reg: (w(4) != u32::MAX).then_some(c),
})?,
ExprOp::StrTrim => L::StrTrim {
a,
mode: TrimMode::from_wire(sel).ok_or_else(bad_sel)?,
set_idx: ConstIdx(w(3)),
},
ExprOp::StrLike => L::StrLike {
src: a,
pat_idx: ConstIdx(w(3)),
ci: flag(opw, sel)?,
},
ExprOp::StrConcat => L::StrConcat { a, b, skip_null: flag(opw, sel)? },
ExprOp::IntToStr => no_sel(L::IntToStr { a })?,
ExprOp::FloatToStr => no_sel(L::FloatToStr { a })?,
ExprOp::StrToInt => L::StrToInt { a, fi: cast_target(opw, sel)? },
ExprOp::StrToFloat => no_sel(L::StrToFloat { a })?,
ExprOp::StrSide => L::StrSide { src: a, n_reg: b, left: flag(opw, sel)? },
ExprOp::StrPos => no_sel(L::StrPos { hay: a, needle: b })?,
ExprOp::StrReverse => no_sel(L::StrReverse { a })?,
ExprOp::StrReplace => no_sel(L::StrReplace { s: a, from: b, to: c })?,
ExprOp::StrPad => L::StrPad {
s: a,
n_reg: b,
fill: c,
left: flag(opw, sel)?,
},
ExprOp::StrSplitPart => no_sel(L::StrSplitPart { s: a, delim: b, n_reg: c })?,
})
}
pub(crate) fn is_identity_map(&self, in_schema: &dyn SchemaFacts, out_schema: &dyn SchemaFacts) -> bool {
in_schema.same_layout(out_schema)
&& self.instrs.is_empty()
&& self
.sinks
.iter()
.copied()
.eq((0..in_schema.num_payload_cols()).map(|pi| Sink::Col(in_schema.payload_col_idx(pi) as u32)))
}
pub(crate) fn resolve_program(
&self,
schema: &dyn SchemaFacts,
sink_read: ReadAs,
) -> Result<ResolvedProgram, ExprValidateErr> {
use Instr as I;
use LogicalInstr as L;
let ProgramFacts { bit_only, bool_pack, no_nulls, reg_u64 } = self.analyze(schema, sink_read)?;
let str_class = self.str_class;
let payload_slot = |col: u32| match schema.payload_slot(col as usize) {
Some(slot) => Ok(slot as u8),
None => Err(ExprValidateErr::ColNotPayload { col }),
};
let mut instrs = Vec::with_capacity(self.instrs.len());
let n_pool = self.const_strings.len();
let mut int_sets: Vec<Option<Vec<i64>>> = vec![None; n_pool];
let mut trim_sets: Vec<Option<[u64; 4]>> = vec![None; n_pool];
let mut const_cells: Vec<Option<[u8; 16]>> = vec![None; n_pool];
let mut const_spans: Vec<Option<(u32, u32)>> = vec![None; n_pool];
let mut like_matchers: Vec<LikeMatcher> = Vec::new();
let mut const_arena: Vec<u8> = Vec::new();
let mut const_regs: Vec<(u16, i64)> = Vec::new();
let mut const_str_regs: Vec<(u16, u32, u32)> = Vec::new();
let mut str_cols: u64 = 0;
let is_u64 = |r: u16| (reg_u64 >> r) & 1 != 0;
let int_reg = |r: Reg| IntReg { reg: r.0, signed: !is_u64(r.0) };
let nullable_slots = schema.nullable_payload_slots();
let num_regs = self.instrs.len() as u32;
for (i, li) in self.instrs.iter().copied().enumerate() {
let dst = i as u16;
let resolved = match li {
L::LoadCol { col } => {
let loc = locate_col(schema, col, ColKind::ScalarCol)?;
match (ScalarKind::from_type_code(loc.type_code()), loc) {
(Some(ScalarKind::Int(fi)), ColumnLocator::Pk { byte_off, .. }) => {
I::LoadPk { off: byte_off, fi }
}
(Some(ScalarKind::Int(fi)), ColumnLocator::Payload { slot, .. }) => {
I::LoadPayloadInt { pi: slot, fi }
}
(Some(ScalarKind::F64), ColumnLocator::Payload { slot, .. }) => {
I::LoadPayloadInt { pi: slot, fi: FixedInt::I64 }
}
(Some(ScalarKind::F32), ColumnLocator::Payload { slot, .. }) => I::LoadPayloadF32 { pi: slot },
(Some(ScalarKind::F32 | ScalarKind::F64), ColumnLocator::Pk { .. }) => {
return Err(ExprValidateErr::ColNotPayload { col })
}
(None, _) => {
return Err(ExprValidateErr::ColKindMismatch {
col,
type_code: loc.type_code(),
want: SCALAR_COL,
})
}
}
}
L::LoadConst { val, .. } => {
const_regs.push((dst, val));
continue;
}
L::IntArith { op, a: Reg(a), b: Reg(b) } => I::IntArith { op, a, b, signed: !is_u64(dst) },
L::FloatArith { op, a: Reg(a), b: Reg(b) } => I::FloatArith { op, a, b },
L::Cmp { op, a: Reg(a), b: Reg(b) } => {
let (op, a, b, order) = match (is_u64(a), is_u64(b)) {
(false, false) => (op, a, b, IntOrder::Signed),
(true, true) => (op, a, b, IntOrder::Unsigned),
(true, false) => (op, a, b, IntOrder::UnsignedSigned),
(false, true) => (op.converse(), b, a, IntOrder::UnsignedSigned),
};
I::Cmp { op, a, b, order }
}
L::FCmp { op, a: Reg(a), b: Reg(b) } => I::FCmp { op, a, b },
L::FloatUnary { op, a: Reg(a) } => I::FloatUnary { op, a },
L::IntUnary { op, a: Reg(a) } => I::IntUnary { op, a, signed: !is_u64(a) },
L::Calendar { op, a: Reg(a), micros } => I::Calendar { op, a, micros },
L::FloatToF32 { a: Reg(a) } => I::FloatToF32 { a },
L::FloatToInt { a: Reg(a), fi } => I::FloatToInt { a, fi },
L::IntCast { a: Reg(a), fi } => I::IntCast { a, fi, src_signed: !is_u64(a) },
L::IntMinMax2 { a: Reg(a), b: Reg(b), is_max } => I::IntMinMax2 { a, b, is_max, signed: !is_u64(dst) },
L::FloatMinMax2 { a: Reg(a), b: Reg(b), is_max } => I::FloatMinMax2 { a, b, is_max },
L::IntToFloat { a: Reg(a) } => I::IntToFloat { a, signed: !is_u64(a) },
L::Select { cond: Reg(cond), a: Reg(a), b: Reg(b) } => I::Select { cond, a, b },
L::LoadNull => I::LoadNull,
L::BoolBinary { a: Reg(a), b: Reg(b), is_or } => I::BoolBinary { a, b, is_or },
L::BoolNot { a: Reg(a) } => I::BoolNot { a },
L::IsNull { col, invert } => match locate_col(schema, col, ColKind::AnyCol)? {
ColumnLocator::Payload { slot, .. } if gnitz_wire::null_word_get(nullable_slots, slot as usize) => {
I::IsNull { pi: slot, invert }
}
_ => {
const_regs.push((dst, invert as i64));
continue;
}
},
L::IsNullReg { a: Reg(a), invert } => I::IsNullReg { a, invert },
L::StrColConst { op, col, const_idx: ConstIdx(const_idx) } => {
let ci = const_idx as usize;
const_cells[ci]
.get_or_insert_with(|| encode_german_string(&self.const_strings[ci], &mut const_arena));
I::StrColConst { op, pi: payload_slot(col)?, const_idx }
}
L::StrColCol { op, col_a, col_b } => I::StrColCol {
op,
pi_a: payload_slot(col_a)?,
pi_b: payload_slot(col_b)?,
},
L::IntInSet {
value_reg: Reg(value_reg),
set_idx: ConstIdx(set_idx),
} => {
let si = set_idx as usize;
int_sets[si].get_or_insert_with(|| decode_int_set(&self.const_strings[si]));
I::IntInSet { value_reg, set_idx }
}
L::LoadColStr { col } => {
let pi = payload_slot(col)?;
str_cols |= 1u64 << pi;
I::LoadColStr { pi }
}
L::LoadConstStr { const_idx: ConstIdx(const_idx) } => {
let ci = const_idx as usize;
let (off, len) = *const_spans[ci].get_or_insert_with(|| {
let bytes = &self.const_strings[ci];
let span = (const_arena.len() as u32, bytes.len() as u32);
const_arena.extend_from_slice(bytes);
span
});
const_str_regs.push((dst, off, len));
continue;
}
L::LoadNullStr => I::LoadNullStr,
L::StrSelect { cond: Reg(cond), a: Reg(a), b: Reg(b) } => I::StrSelect { cond, a, b },
L::StrCmp { op, a: Reg(a), b: Reg(b) } => I::StrCmp { op, a, b },
L::StrLen { a: Reg(a), chars } => I::StrLen { a, chars },
L::StrCase { a: Reg(a), upper } => I::StrCase { a, upper },
L::StrSubstr { src: Reg(src), start_reg, len_reg } => I::StrSubstr {
src,
start: int_reg(start_reg),
len: len_reg.map(int_reg),
},
L::StrTrim {
a: Reg(a),
mode,
set_idx: ConstIdx(set_idx),
} => {
let si = set_idx as usize;
trim_sets[si].get_or_insert_with(|| {
let mut table = [0u64; 4];
for &byte in &self.const_strings[si] {
table[(byte >> 6) as usize] |= 1u64 << (byte & 63);
}
table
});
I::StrTrim { a, mode, set_idx }
}
L::StrLike {
src: Reg(src),
pat_idx: ConstIdx(pat_idx),
ci,
} => {
let pattern = &self.const_strings[pat_idx as usize];
let matcher_idx = like_matchers.len() as u32;
like_matchers.push(LikeMatcher::compile(pattern, ci));
I::StrLike { src, matcher_idx }
}
L::StrConcat { a: Reg(a), b: Reg(b), skip_null } => I::StrConcat { a, b, skip_null },
L::IntToStr { a: Reg(a) } => I::IntToStr { a, signed: !is_u64(a) },
L::FloatToStr { a: Reg(a) } => I::FloatToStr { a },
L::StrToInt { a: Reg(a), fi } => I::StrToInt { a, fi },
L::StrToFloat { a: Reg(a) } => I::StrToFloat { a },
L::StrSide { src: Reg(src), n_reg, left } => I::StrSide { src, n: int_reg(n_reg), left },
L::StrPos { hay: Reg(hay), needle: Reg(needle) } => I::StrPos { hay, needle },
L::StrReverse { a: Reg(a) } => I::StrReverse { a },
L::StrReplace { s: Reg(s), from: Reg(from), to: Reg(to) } => I::StrReplace { s, from, to },
L::StrPad { s: Reg(s), n_reg, fill: Reg(fill), left } => I::StrPad { s, n: int_reg(n_reg), fill, left },
L::StrSplitPart { s: Reg(s), delim: Reg(delim), n_reg } => {
I::StrSplitPart { s, delim, n: int_reg(n_reg) }
}
};
instrs.push((dst, resolved));
}
Ok(ResolvedProgram {
no_nulls,
nullable_slots,
bit_only_mask: bit_only,
bool_pack_mask: bool_pack,
instrs,
const_regs,
const_str_regs,
const_cells: dense(const_cells),
int_sets: dense(int_sets),
trim_sets: dense(trim_sets),
like_matchers,
const_arena,
str_lanes: MAX_REGS as u32 - str_class.leading_zeros(),
scalar_lanes: (0..num_regs)
.rev()
.find(|&r| (str_class >> r) & 1 == 0)
.map_or(0, |r| r + 1),
str_cols,
reg_u64,
})
}
fn analyze(&self, schema: &dyn SchemaFacts, sink_read: ReadAs) -> Result<ProgramFacts, ExprValidateErr> {
let (mut bool_produced, mut non_bool_read, mut bool_input) = (0u64, 0u64, 0u64);
let mut read_as = |reg: Reg, read: ReadAs| match read {
ReadAs::Bool => bool_input |= 1u64 << reg.0,
ReadAs::NullBit => {}
ReadAs::Value | ReadAs::Str => non_bool_read |= 1u64 << reg.0,
};
let nullable_slots = schema.nullable_payload_slots();
let mut no_nulls = true;
let mut reg_u64 = 0u64;
for (i, li) in self.instrs.iter().enumerate() {
let ops = operands(li);
let mut col_is_u64 = false;
for &(col, kind) in ops.cols.iter().flatten() {
let loc = locate_col(schema, col, kind)?;
col_is_u64 |= loc.type_code() == TypeCode::U64;
no_nulls &= kind.type_test().is_none() || !loc.is_null_word(nullable_slots);
}
match ops.write {
WriteAs::Bool => bool_produced |= 1u64 << i,
WriteAs::Value(rule) => reg_u64 |= (u64_verdict(rule, &ops, col_is_u64, reg_u64) as u64) << i,
WriteAs::Str => {}
}
no_nulls &= !ops.makes_null;
for &(reg, read) in ops.reads.iter().flatten() {
read_as(reg, read);
}
}
for reg in self.sinks.iter().filter_map(|s| s.reg()) {
read_as(reg, sink_read);
}
Ok(ProgramFacts {
bit_only: bool_produced & !non_bool_read,
bool_pack: bool_input,
no_nulls,
reg_u64,
})
}
pub(crate) fn is_str(&self, r: Reg) -> bool {
(self.str_class >> r.0) & 1 != 0
}
}
fn check_pool_entry(idx: ConstIdx, want: PoolEntry, const_strings: &[Vec<u8>]) -> Result<(), ExprValidateErr> {
check_const_idx(idx.0, const_strings.len())?;
let bytes = &const_strings[idx.0 as usize];
let ok = match want {
PoolEntry::Bytes => true,
PoolEntry::Text => std::str::from_utf8(bytes).is_ok(),
PoolEntry::LikePattern => LikePattern::is_encoding(bytes),
PoolEntry::TrimSet => bytes.is_ascii(),
PoolEntry::IntSet => int_set_is_canonical(bytes),
};
match ok {
true => Ok(()),
false => Err(ExprValidateErr::PoolEntryMalformed { const_idx: idx.0, want }),
}
}
fn check_const_idx(const_idx: u32, n: usize) -> Result<(), ExprValidateErr> {
if (const_idx as usize) < n {
Ok(())
} else {
Err(ExprValidateErr::ConstIdxOutOfRange { const_idx, n })
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub(crate) enum ReadAs {
Value,
Str,
Bool,
NullBit,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum WriteAs {
Value(U64Rule),
Bool,
Str,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum U64Rule {
Fixed(bool),
FromOperands,
FromColType,
}
impl ReadAs {
fn wants_str(self) -> bool {
matches!(self, ReadAs::Str)
}
}
struct Operands {
write: WriteAs,
reads: [Option<(Reg, ReadAs)>; MAX_READS],
cols: [Option<(u32, ColKind)>; MAX_COL_OPERANDS],
pool: Option<(ConstIdx, PoolEntry)>,
makes_null: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PoolEntry {
Bytes,
Text,
LikePattern,
TrimSet,
IntSet,
}
const MAX_READS: usize = 3;
const MAX_COL_OPERANDS: usize = 2;
fn writes(write: WriteAs) -> Operands {
Operands {
write,
reads: [None; MAX_READS],
cols: [None; MAX_COL_OPERANDS],
pool: None,
makes_null: false,
}
}
impl Operands {
fn may_null(mut self) -> Self {
self.makes_null = true;
self
}
fn reading(mut self, reg: Reg, read: ReadAs) -> Self {
let slot = self
.reads
.iter()
.position(Option::is_none)
.expect("an opcode reads at most MAX_READS registers");
self.reads[slot] = Some((reg, read));
self
}
fn reading_opt(self, reg: Option<Reg>, read: ReadAs) -> Self {
match reg {
Some(r) => self.reading(r, read),
None => self,
}
}
fn with_pool(mut self, idx: ConstIdx, want: PoolEntry) -> Self {
self.pool = Some((idx, want));
self
}
fn on_col(mut self, col: u32, kind: ColKind) -> Self {
let slot = self
.cols
.iter()
.position(Option::is_none)
.expect("an opcode addresses at most MAX_COL_OPERANDS columns");
self.cols[slot] = Some((col, kind));
self
}
}
fn operands(li: &LogicalInstr) -> Operands {
use LogicalInstr as L;
use ReadAs::{Bool as RBool, Str as RStr, Value as RVal};
use U64Rule::{Fixed, FromColType, FromOperands};
use WriteAs::{Bool as WBool, Str as WStr, Value as WVal};
match *li {
L::IntArith { op, a, b } => {
let ops = writes(WVal(FromOperands)).reading(a, RVal).reading(b, RVal);
match op {
IntArithOp::Div | IntArithOp::Mod => ops.may_null(),
IntArithOp::Add | IntArithOp::Sub | IntArithOp::Mul => ops,
}
}
L::FloatArith { op, a, b } => {
let ops = writes(WVal(Fixed(false))).reading(a, RVal).reading(b, RVal);
match op {
FloatArithOp::Div => ops.may_null(),
FloatArithOp::Add | FloatArithOp::Sub | FloatArithOp::Mul | FloatArithOp::Pow => ops,
}
}
L::IntMinMax2 { a, b, is_max: _ } => writes(WVal(FromOperands)).reading(a, RVal).reading(b, RVal),
L::FloatMinMax2 { a, b, is_max: _ } => writes(WVal(Fixed(false))).reading(a, RVal).reading(b, RVal),
L::Cmp { op: _, a, b } | L::FCmp { op: _, a, b } => writes(WBool).reading(a, RVal).reading(b, RVal),
L::BoolBinary { a, b, is_or: _ } => writes(WBool).reading(a, RBool).reading(b, RBool),
L::BoolNot { a } => writes(WBool).reading(a, RBool),
L::IntUnary { op, a } => match op {
IntUnaryOp::Sign => writes(WVal(Fixed(false))).reading(a, RVal),
IntUnaryOp::Neg | IntUnaryOp::Abs => writes(WVal(FromOperands)).reading(a, RVal),
},
L::FloatUnary { op: _, a } | L::IntToFloat { a } => writes(WVal(Fixed(false))).reading(a, RVal),
L::Calendar { op, a, micros: _ } => {
let ops = writes(WVal(Fixed(false))).reading(a, RVal);
if op.may_null() {
ops.may_null()
} else {
ops
}
}
L::FloatToF32 { a } => writes(WVal(Fixed(false))).reading(a, RVal).may_null(),
L::FloatToInt { a, fi } | L::IntCast { a, fi } => {
writes(WVal(Fixed(fi == FixedInt::U64))).reading(a, RVal).may_null()
}
L::Select { cond, a, b } => writes(WVal(FromOperands))
.reading(cond, RBool)
.reading(a, RVal)
.reading(b, RVal),
L::IntInSet { value_reg, set_idx } => writes(WBool)
.reading(value_reg, RVal)
.with_pool(set_idx, PoolEntry::IntSet),
L::LoadConst { val: _, unsigned } => writes(WVal(Fixed(unsigned))),
L::LoadNull => writes(WVal(Fixed(false))).may_null(),
L::LoadCol { col } => writes(WVal(FromColType)).on_col(col, ColKind::ScalarCol),
L::IsNull { col, invert: _ } => writes(WBool).on_col(col, ColKind::AnyCol),
L::IsNullReg { a, invert: _ } => writes(WBool).reading(a, ReadAs::NullBit),
L::StrColConst { op: _, col, const_idx } => writes(WBool)
.on_col(col, ColKind::StringPayload)
.with_pool(const_idx, PoolEntry::Bytes),
L::StrColCol { op: _, col_a, col_b } => writes(WBool)
.on_col(col_a, ColKind::StringPayload)
.on_col(col_b, ColKind::StringPayload),
L::LoadColStr { col } => writes(WStr).on_col(col, ColKind::StringPayload),
L::LoadConstStr { const_idx } => writes(WStr).with_pool(const_idx, PoolEntry::Text),
L::LoadNullStr => writes(WStr).may_null(),
L::IntToStr { a } | L::FloatToStr { a } => writes(WStr).reading(a, RVal),
L::StrLen { a, chars: _ } => writes(WVal(Fixed(false))).reading(a, RStr),
L::StrToFloat { a } => writes(WVal(Fixed(false))).reading(a, RStr).may_null(),
L::StrToInt { a, fi } => writes(WVal(Fixed(fi == FixedInt::U64))).reading(a, RStr).may_null(),
L::StrCmp { op: _, a, b } => writes(WBool).reading(a, RStr).reading(b, RStr),
L::StrLike { src, pat_idx, ci: _ } => writes(WBool)
.reading(src, RStr)
.with_pool(pat_idx, PoolEntry::LikePattern),
L::StrCase { a, upper: _ } => writes(WStr).reading(a, RStr),
L::StrTrim { a, mode: _, set_idx } => writes(WStr).reading(a, RStr).with_pool(set_idx, PoolEntry::TrimSet),
L::StrConcat { a, b, skip_null: _ } => writes(WStr).reading(a, RStr).reading(b, RStr).may_null(),
L::StrSelect { cond, a, b } => writes(WStr).reading(cond, RBool).reading(a, RStr).reading(b, RStr),
L::StrSubstr { src, start_reg, len_reg } => {
let ops = writes(WStr)
.reading(src, RStr)
.reading(start_reg, RVal)
.reading_opt(len_reg, RVal);
if len_reg.is_some() {
ops.may_null()
} else {
ops
}
}
L::StrSide { src, n_reg, left: _ } => writes(WStr).reading(src, RStr).reading(n_reg, RVal),
L::StrReverse { a } => writes(WStr).reading(a, RStr),
L::StrPos { hay, needle } => writes(WVal(Fixed(false))).reading(hay, RStr).reading(needle, RStr),
L::StrReplace { s, from, to } => writes(WStr)
.reading(s, RStr)
.reading(from, RStr)
.reading(to, RStr)
.may_null(),
L::StrPad { s, n_reg, fill, left: _ } => writes(WStr)
.reading(s, RStr)
.reading(n_reg, RVal)
.reading(fill, RStr)
.may_null(),
L::StrSplitPart { s, delim, n_reg } => writes(WStr)
.reading(s, RStr)
.reading(delim, RStr)
.reading(n_reg, RVal)
.may_null(),
}
}
fn cast_target(op: u32, selector: u32) -> Result<FixedInt, ExprValidateErr> {
u8::try_from(selector)
.ok()
.and_then(TypeCode::from_wire)
.and_then(FixedInt::exact)
.ok_or(ExprValidateErr::BadSelector { op, selector })
}
fn flag(op: u32, selector: u32) -> Result<bool, ExprValidateErr> {
match selector {
0 => Ok(false),
1 => Ok(true),
_ => Err(ExprValidateErr::BadSelector { op, selector }),
}
}
fn locate_col(s: &dyn SchemaFacts, col: u32, need: ColKind) -> Result<ColumnLocator, ExprValidateErr> {
let Some(loc) = s.try_locate(col as usize) else {
return Err(ExprValidateErr::ColOutOfRange { col, num_columns: s.num_columns() });
};
if need.payload_only() && matches!(loc, ColumnLocator::Pk { .. }) {
return Err(ExprValidateErr::ColNotPayload { col });
}
let type_code = loc.type_code();
match need.type_test() {
Some((accepts, want)) if !accepts(type_code) => Err(ExprValidateErr::ColKindMismatch { col, type_code, want }),
_ => Ok(loc),
}
}
fn dense<T: Default>(slots: Vec<Option<T>>) -> Vec<T> {
slots.into_iter().map(Option::unwrap_or_default).collect()
}
fn int_set_is_canonical(bytes: &[u8]) -> bool {
bytes.len().is_multiple_of(8)
&& bytes
.as_chunks::<8>()
.0
.windows(2)
.all(|w| i64::from_le_bytes(w[0]) < i64::from_le_bytes(w[1]))
}
fn decode_int_set(bytes: &[u8]) -> Vec<i64> {
debug_assert!(int_set_is_canonical(bytes), "construction rejects a non-canonical pool");
let mut v = Vec::new();
gnitz_wire::extend_from_le_bytes(&mut v, bytes);
v
}
pub struct NullPerm {
first: NullMove,
rest: Vec<NullMove>,
}
#[derive(Clone, Copy)]
struct NullMove {
mask: u64,
up: u8,
down: u8,
}
impl NullMove {
#[inline(always)]
fn apply(self, word: u64) -> u64 {
((word & self.mask) << self.up) >> self.down
}
}
impl NullPerm {
pub fn new(copies: &[ColCopy], nullable: u64) -> Self {
let mut moves: Vec<NullMove> = Vec::new();
for c in copies {
let ColumnLocator::Payload { slot: src, .. } = c.src else {
continue;
};
if !gnitz_wire::null_word_get(nullable, src as usize) {
continue;
}
let dst = c.slot as u8;
let (up, down) = (dst.saturating_sub(src), src.saturating_sub(dst));
match moves.iter_mut().find(|m| (m.up, m.down) == (up, down)) {
Some(m) => m.mask |= 1u64 << src,
None => moves.push(NullMove { mask: 1u64 << src, up, down }),
}
}
moves.sort_by_key(|m| m.up.max(m.down));
let mut moves = moves.into_iter();
let first = moves.next().unwrap_or(NullMove { mask: 0, up: 0, down: 0 });
NullPerm { first, rest: moves.collect() }
}
#[inline(always)]
pub fn apply(&self, word: u64) -> u64 {
self.rest
.iter()
.fold(self.first.apply(word), |bits, m| bits | m.apply(word))
}
pub(crate) fn write_rows(&self, in_null_bmp: &[u8], src_start: usize, out: &mut [u8], dst_base: usize, n: usize) {
let dst = &mut out[dst_base * 8..(dst_base + n) * 8];
let src = in_null_bmp[src_start * 8..(src_start + n) * 8].as_chunks::<8>().0;
let word = |w: &[u8; 8]| u64::from_le_bytes(*w);
let (first, rest) = (self.first, &self.rest[..]);
if first.mask == 0 {
return dst.fill(0);
}
let block = if rest.is_empty() { n.max(1) } else { 256 };
for (d, s) in dst.as_chunks_mut::<8>().0.chunks_mut(block).zip(src.chunks(block)) {
if first.up == 0 && first.down == 0 {
for (d, s) in d.iter_mut().zip(s) {
*d = (word(s) & first.mask).to_le_bytes();
}
} else {
for (d, s) in d.iter_mut().zip(s) {
*d = first.apply(word(s)).to_le_bytes();
}
}
for &mv in rest {
for (d, s) in d.iter_mut().zip(s) {
*d = (word(d) | mv.apply(word(s))).to_le_bytes();
}
}
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ColCopy {
pub src: ColumnLocator,
pub slot: usize,
pub width: usize,
}
#[derive(Debug, PartialEq)]
pub(crate) struct ScalarEmit {
pub(crate) reg: usize,
pub(crate) slot: usize,
pub(crate) width: EmitWidth,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) enum EmitWidth {
W1,
W2,
W4,
W8,
}
impl EmitWidth {
fn for_slot(type_code: TypeCode, src: &LogicalInstr) -> Option<Self> {
match type_code.wire_stride() {
8 => Some(EmitWidth::W8),
w if type_code.is_fixed_int() && src.range_check().map(FixedInt::width) == Some(w) => match w {
1 => Some(EmitWidth::W1),
2 => Some(EmitWidth::W2),
4 => Some(EmitWidth::W4),
_ => None,
},
_ => None,
}
}
pub(crate) fn bytes(self) -> usize {
match self {
EmitWidth::W1 => 1,
EmitWidth::W2 => 2,
EmitWidth::W4 => 4,
EmitWidth::W8 => 8,
}
}
}
#[derive(Debug, PartialEq)]
pub(crate) struct StrEmit {
pub(crate) reg: usize,
pub(crate) slot: usize,
}
pub(crate) struct MapSinks {
pub(crate) copies: Vec<ColCopy>,
pub(crate) null_perm: NullPerm,
pub(crate) scalar_emits: Vec<ScalarEmit>,
pub(crate) str_emits: Vec<StrEmit>,
}
impl LogicalProgram {
pub(crate) fn map_sinks(
&self,
in_schema: &dyn SchemaFacts,
out_schema: &dyn SchemaFacts,
) -> Result<MapSinks, ExprValidateErr> {
use ExprValidateErr as E;
let out_slots = out_schema.payload_locators();
if self.sinks.len() != out_slots.len() {
return Err(E::OutputSlotCountMismatch {
sinks: self.sinks.len(),
num_payload_cols: out_slots.len(),
});
}
let (mut copies, mut scalar_emits, mut str_emits) = (Vec::new(), Vec::new(), Vec::new());
for (slot, (sink, out_loc)) in self.sinks.iter().zip(out_slots).enumerate() {
let (out, type_code) = (slot as u32, out_loc.type_code());
match *sink {
Sink::Col(col) => {
let src = locate_col(in_schema, col, ColKind::AnyCol)?;
let src_tc = src.type_code();
if src_tc != type_code && !src_tc.is_widening_promotion(type_code) {
return Err(E::CopyTypeMismatch { col, src_tc, out, out_tc: type_code });
}
copies.push(ColCopy { src, slot, width: out_loc.size() });
}
Sink::Reg(r) if self.is_str(r) != type_code.is_german_string() => {
return Err(E::EmitClassMismatch { out, type_code })
}
Sink::Reg(r) if self.is_str(r) => str_emits.push(StrEmit { reg: r.0 as usize, slot }),
Sink::Reg(r) => {
let width = EmitWidth::for_slot(type_code, &self.instrs[r.0 as usize])
.ok_or(E::EmitSlotWidth { out, type_code })?;
scalar_emits.push(ScalarEmit { reg: r.0 as usize, slot, width });
}
}
}
let null_perm = NullPerm::new(&copies, in_schema.nullable_payload_slots());
Ok(MapSinks {
copies,
null_perm,
scalar_emits,
str_emits,
})
}
}
pub(crate) struct ResolvedProgram {
pub(crate) instrs: Vec<(u16, Instr)>,
pub(crate) const_regs: Vec<(u16, i64)>,
pub(crate) const_str_regs: Vec<(u16, u32, u32)>,
pub(crate) const_cells: Vec<[u8; 16]>,
pub(crate) int_sets: Vec<Vec<i64>>,
pub(crate) trim_sets: Vec<[u64; 4]>,
pub(crate) like_matchers: Vec<LikeMatcher>,
pub(crate) const_arena: Vec<u8>,
pub(crate) str_cols: u64,
pub(crate) str_lanes: u32,
pub(crate) scalar_lanes: u32,
pub(crate) no_nulls: bool,
pub(crate) nullable_slots: u64,
bit_only_mask: u64,
bool_pack_mask: u64,
pub(crate) reg_u64: u64,
}
impl ResolvedProgram {
pub(crate) fn num_regs(&self) -> usize {
self.str_lanes.max(self.scalar_lanes) as usize
}
pub(crate) fn is_bit_only(&self, reg: usize) -> bool {
(self.bit_only_mask >> reg) & 1 != 0
}
pub(crate) fn needs_bool_pack(&self, reg: usize) -> bool {
(self.bool_pack_mask >> reg) & 1 != 0
}
}
struct ProgramFacts {
bit_only: u64,
bool_pack: u64,
no_nulls: bool,
reg_u64: u64,
}
fn u64_verdict(rule: U64Rule, ops: &Operands, col_is_u64: bool, so_far: u64) -> bool {
match rule {
U64Rule::Fixed(v) => v,
U64Rule::FromOperands => ops
.reads
.iter()
.flatten()
.any(|&(reg, read)| read == ReadAs::Value && (so_far >> reg.0) & 1 != 0),
U64Rule::FromColType => col_is_u64,
}
}
#[cfg(test)]
#[path = "tests/program.rs"]
mod tests;