use crate::ir::analysis::ConstraintInfo;
use crate::ir::bytecode::{LeafRef, LEAF_SOURCE_MAIN_LOCAL, LEAF_SOURCE_PREPROCESSED_LOCAL};
use crate::ir::chunker::Chunk;
use crate::ir::dag::{ConstraintDag, DagNode, TraceSource};
use crate::ir::lowering::ColumnTilePlan;
use crate::F;
pub const COEFF_KIND_CONST: u32 = 0;
pub const COEFF_KIND_PUBLIC: u32 = 1;
pub const COEFF_KIND_MASK: u32 = 0x7FFF_FFFF;
pub const COEFF_NEGATE_BIT: u32 = 1u32 << 31;
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct ColumnTermEntry {
pub leaf_idx: u32,
pub coeff_kind: u32,
pub coeff_idx: u32,
pub alpha_idx: u32,
}
#[derive(Debug, Default, Clone)]
pub struct ColumnTileBytecode {
pub leaves: Vec<LeafRef>,
pub consts: Vec<F>,
pub publics: Vec<u32>,
pub terms: Vec<ColumnTermEntry>,
pub n_constraints: u32,
}
pub fn lower_column_tile(
chunk: &Chunk,
_constraints: &[ConstraintInfo],
dag: &ConstraintDag,
plan: &ColumnTilePlan,
) -> Option<ColumnTileBytecode> {
let mut bc = ColumnTileBytecode {
n_constraints: chunk.constraint_indices.len() as u32,
..ColumnTileBytecode::default()
};
let mut leaf_lookup: std::collections::HashMap<(u8, u32), u32> = Default::default();
let mut const_lookup: std::collections::HashMap<u32, u32> = Default::default();
let mut public_lookup: std::collections::HashMap<u32, u32> = Default::default();
for t in &plan.terms {
let (src_byte, col) = match dag.nodes[t.leaf_node as usize] {
DagNode::InputLeaf { source, col } => {
let s = match source {
TraceSource::PreprocessedLocal => LEAF_SOURCE_PREPROCESSED_LOCAL,
TraceSource::MainLocal => LEAF_SOURCE_MAIN_LOCAL,
};
(s, col)
}
_ => return None,
};
let leaf_idx = *leaf_lookup.entry((src_byte, col)).or_insert_with(|| {
let i = bc.leaves.len() as u32;
bc.leaves.push(LeafRef { source: src_byte, _pad: 0, col });
i
});
let (coeff_kind, coeff_idx) = match dag.nodes[t.coeff_node as usize] {
DagNode::ConstF { value } => {
use slop_algebra::PrimeField32;
let key = value.as_canonical_u32();
let idx = *const_lookup.entry(key).or_insert_with(|| {
let i = bc.consts.len() as u32;
bc.consts.push(value);
i
});
(COEFF_KIND_CONST, idx)
}
DagNode::PublicValue { idx } => {
let pidx = *public_lookup.entry(idx).or_insert_with(|| {
let i = bc.publics.len() as u32;
bc.publics.push(idx);
i
});
(COEFF_KIND_PUBLIC, pidx)
}
_ => return None,
};
let kind_encoded = if t.negate { coeff_kind | COEFF_NEGATE_BIT } else { coeff_kind };
bc.terms.push(ColumnTermEntry {
leaf_idx,
coeff_kind: kind_encoded,
coeff_idx,
alpha_idx: t.alpha_idx,
});
}
Some(bc)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ir::analysis::{analyze_constraints, ConstraintShape};
use crate::ir::chunker::Chunk;
use crate::ir::dag::{ConstraintRef, DagNode, TraceSource};
use crate::ir::lowering::{enumerate_lowerings, Lowering};
use slop_algebra::AbstractField;
use std::collections::HashSet;
#[test]
fn lower_column_tile_handles_subf_sign() {
let mut nodes = Vec::new();
let c0 = nodes.len() as u32;
nodes.push(DagNode::ConstF { value: F::from_canonical_u32(7) });
let x0 = nodes.len() as u32;
nodes.push(DagNode::InputLeaf { source: TraceSource::MainLocal, col: 0 });
let t0 = nodes.len() as u32;
nodes.push(DagNode::MulF { a: c0, b: x0 });
let c1 = nodes.len() as u32;
nodes.push(DagNode::ConstF { value: F::from_canonical_u32(11) });
let x1 = nodes.len() as u32;
nodes.push(DagNode::InputLeaf { source: TraceSource::MainLocal, col: 1 });
let t1 = nodes.len() as u32;
nodes.push(DagNode::MulF { a: c1, b: x1 });
let root = nodes.len() as u32;
nodes.push(DagNode::SubF { a: t0, b: t1 });
let dag = ConstraintDag {
nodes,
constraints: vec![ConstraintRef { root, alpha_index: 0 }],
preprocessed_width: 0,
main_width: 2,
};
let infos = analyze_constraints(&dag);
assert!(matches!(infos[0].shape, ConstraintShape::LinearWeightedSum));
let mut leafset = HashSet::new();
for &leaf in &infos[0].column_leaves {
leafset.insert(leaf);
}
let chunk = Chunk {
constraint_indices: vec![0],
leafset,
depth_max: infos[0].depth,
shape: ConstraintShape::LinearWeightedSum,
};
let lowerings = enumerate_lowerings(&chunk, &infos, &dag);
let plan = lowerings
.iter()
.find_map(|l| match l {
Lowering::ColumnTile(p) => Some(p),
_ => None,
})
.expect("ColumnTile lowering should apply to LinearWeightedSum chunk");
let bc = lower_column_tile(&chunk, &infos, &dag, plan)
.expect("ColumnTile bytecode should lower successfully");
assert_eq!(bc.terms.len(), 2, "two terms expected for `c0*x0 - c1*x1`");
let kind0 = bc.terms[0].coeff_kind & COEFF_KIND_MASK;
let neg0 = (bc.terms[0].coeff_kind & COEFF_NEGATE_BIT) != 0;
assert_eq!(kind0, COEFF_KIND_CONST);
assert!(!neg0, "first term (additive) must not be negated");
let kind1 = bc.terms[1].coeff_kind & COEFF_KIND_MASK;
let neg1 = (bc.terms[1].coeff_kind & COEFF_NEGATE_BIT) != 0;
assert_eq!(kind1, COEFF_KIND_CONST);
assert!(neg1, "right-of-SubF term must carry the negate flag");
}
#[test]
fn lower_column_tile_handles_nested_subf() {
let mut nodes = Vec::new();
let c0 = nodes.len() as u32;
nodes.push(DagNode::ConstF { value: F::from_canonical_u32(2) });
let x0 = nodes.len() as u32;
nodes.push(DagNode::InputLeaf { source: TraceSource::MainLocal, col: 0 });
let t0 = nodes.len() as u32;
nodes.push(DagNode::MulF { a: c0, b: x0 });
let c1 = nodes.len() as u32;
nodes.push(DagNode::ConstF { value: F::from_canonical_u32(3) });
let x1 = nodes.len() as u32;
nodes.push(DagNode::InputLeaf { source: TraceSource::MainLocal, col: 1 });
let t1 = nodes.len() as u32;
nodes.push(DagNode::MulF { a: c1, b: x1 });
let c2 = nodes.len() as u32;
nodes.push(DagNode::ConstF { value: F::from_canonical_u32(5) });
let x2 = nodes.len() as u32;
nodes.push(DagNode::InputLeaf { source: TraceSource::MainLocal, col: 2 });
let t2 = nodes.len() as u32;
nodes.push(DagNode::MulF { a: c2, b: x2 });
let inner = nodes.len() as u32;
nodes.push(DagNode::SubF { a: t1, b: t2 });
let root = nodes.len() as u32;
nodes.push(DagNode::SubF { a: t0, b: inner });
let dag = ConstraintDag {
nodes,
constraints: vec![ConstraintRef { root, alpha_index: 0 }],
preprocessed_width: 0,
main_width: 3,
};
let infos = analyze_constraints(&dag);
assert!(matches!(infos[0].shape, ConstraintShape::LinearWeightedSum));
let mut leafset = HashSet::new();
for &leaf in &infos[0].column_leaves {
leafset.insert(leaf);
}
let chunk = Chunk {
constraint_indices: vec![0],
leafset,
depth_max: infos[0].depth,
shape: ConstraintShape::LinearWeightedSum,
};
let lowerings = enumerate_lowerings(&chunk, &infos, &dag);
let plan = lowerings
.iter()
.find_map(|l| match l {
Lowering::ColumnTile(p) => Some(p),
_ => None,
})
.expect("ColumnTile lowering should apply");
let bc = lower_column_tile(&chunk, &infos, &dag, plan)
.expect("ColumnTile bytecode should lower successfully");
assert_eq!(bc.terms.len(), 3);
let neg = |i: usize| (bc.terms[i].coeff_kind & COEFF_NEGATE_BIT) != 0;
assert!(!neg(0), "t0 = +c0*x0");
assert!(neg(1), "t1 = -c1*x1");
assert!(!neg(2), "t2 = -(-c2*x2) = +c2*x2");
}
}