binius_m3/builder/
table.rs

1// Copyright 2025 Irreducible Inc.
2
3use std::sync::Arc;
4
5use binius_core::{
6	constraint_system::channel::{ChannelId, FlushDirection},
7	oracle::ShiftVariant,
8	transparent::MultilinearExtensionTransparent,
9};
10use binius_field::{
11	arch::OptimalUnderlier,
12	as_packed_field::{PackScalar, PackedType},
13	packed::pack_slice,
14	ExtensionField, TowerField,
15};
16use binius_utils::{
17	checked_arithmetics::{checked_log_2, log2_ceil_usize, log2_strict_usize},
18	sparse_index::SparseIndex,
19};
20
21use super::{
22	channel::Flush,
23	column::{Col, ColumnDef, ColumnInfo, ColumnShape},
24	expr::{Expr, ZeroConstraint},
25	types::B128,
26	upcast_col, ColumnIndex, FlushOpts,
27};
28use crate::builder::column::ColumnId;
29
30pub type TableId = usize;
31
32#[derive(Debug)]
33pub struct TableBuilder<'a, F: TowerField = B128> {
34	namespace: Option<String>,
35	table: &'a mut Table<F>,
36}
37
38impl<'a, F: TowerField> TableBuilder<'a, F> {
39	pub fn new(table: &'a mut Table<F>) -> Self {
40		Self {
41			namespace: None,
42			table,
43		}
44	}
45
46	pub fn with_namespace(&mut self, namespace: impl ToString) -> TableBuilder<'_, F> {
47		TableBuilder {
48			namespace: Some(self.namespaced_name(namespace)),
49			table: self.table,
50		}
51	}
52
53	pub fn id(&self) -> TableId {
54		self.table.id()
55	}
56
57	pub fn add_committed<FSub, const VALUES_PER_ROW: usize>(
58		&mut self,
59		name: impl ToString,
60	) -> Col<FSub, VALUES_PER_ROW>
61	where
62		FSub: TowerField,
63		F: ExtensionField<FSub>,
64	{
65		self.table.new_column(
66			self.namespaced_name(name),
67			ColumnDef::Committed {
68				tower_level: FSub::TOWER_LEVEL,
69			},
70		)
71	}
72
73	pub fn add_committed_multiple<FSub, const VALUES_PER_ROW: usize, const N: usize>(
74		&mut self,
75		name: impl ToString,
76	) -> [Col<FSub, VALUES_PER_ROW>; N]
77	where
78		FSub: TowerField,
79		F: ExtensionField<FSub>,
80	{
81		std::array::from_fn(|i| self.add_committed(format!("{}[{}]", name.to_string(), i)))
82	}
83
84	pub fn add_shifted<FSub, const VALUES_PER_ROW: usize>(
85		&mut self,
86		name: impl ToString,
87		col: Col<FSub, VALUES_PER_ROW>,
88		log_block_size: usize,
89		offset: usize,
90		variant: ShiftVariant,
91	) -> Col<FSub, VALUES_PER_ROW>
92	where
93		FSub: TowerField,
94		F: ExtensionField<FSub>,
95	{
96		assert!(log_block_size <= log2_strict_usize(VALUES_PER_ROW));
97		assert!(offset <= 1 << log_block_size);
98		self.table.new_column(
99			self.namespaced_name(name),
100			ColumnDef::Shifted {
101				col: col.id(),
102				offset,
103				log_block_size,
104				variant,
105			},
106		)
107	}
108
109	pub fn add_packed<FSubSub, const VALUES_PER_ROW_SUB: usize, FSub, const VALUES_PER_ROW: usize>(
110		&mut self,
111		name: impl ToString,
112		col: Col<FSubSub, VALUES_PER_ROW_SUB>,
113	) -> Col<FSub, VALUES_PER_ROW>
114	where
115		FSub: TowerField + ExtensionField<FSubSub>,
116		FSubSub: TowerField,
117		F: ExtensionField<FSub>,
118	{
119		assert!(FSubSub::TOWER_LEVEL < FSub::TOWER_LEVEL);
120		assert!(VALUES_PER_ROW_SUB > VALUES_PER_ROW);
121		assert_eq!(
122			FSub::TOWER_LEVEL + log2_strict_usize(VALUES_PER_ROW),
123			FSubSub::TOWER_LEVEL + log2_strict_usize(VALUES_PER_ROW_SUB)
124		);
125		self.table.new_column(
126			self.namespaced_name(name),
127			ColumnDef::Packed {
128				col: col.id(),
129				log_degree: FSub::TOWER_LEVEL - FSubSub::TOWER_LEVEL,
130			},
131		)
132	}
133
134	pub fn add_computed<FSub, const V: usize>(
135		&mut self,
136		name: impl ToString,
137		expr: Expr<FSub, V>,
138	) -> Col<FSub, V>
139	where
140		FSub: TowerField,
141		F: ExtensionField<FSub>,
142	{
143		let partition_indexes = expr
144			.expr()
145			.vars_usage()
146			.iter()
147			.enumerate()
148			.filter(|(_, &used)| used)
149			.map(|(i, _)| i)
150			.collect::<Vec<_>>();
151		let cols = partition_indexes
152			.iter()
153			.map(|&partition_index| {
154				let partition = &self.table.partitions[partition_id::<V>()];
155				partition.columns[partition_index]
156			})
157			.collect::<Vec<_>>();
158
159		let mut var_remapping = vec![0; expr.expr().n_vars()];
160		for (new_index, &old_index) in partition_indexes.iter().enumerate() {
161			var_remapping[old_index] = new_index;
162		}
163		let remapped_expr = expr
164			.expr()
165			.convert_field()
166			.remap_vars(&var_remapping)
167			.expect("var_remapping should be large enought");
168
169		self.table.new_column(
170			self.namespaced_name(name),
171			ColumnDef::Computed {
172				cols,
173				expr: remapped_expr,
174			},
175		)
176	}
177
178	pub fn add_selected<FSub, const VALUES_PER_ROW: usize>(
179		&mut self,
180		name: impl ToString,
181		col: Col<FSub, VALUES_PER_ROW>,
182		index: usize,
183	) -> Col<FSub, 1>
184	where
185		FSub: TowerField,
186		F: ExtensionField<FSub>,
187	{
188		assert!(index < VALUES_PER_ROW);
189		self.table.new_column(
190			self.namespaced_name(name),
191			ColumnDef::Selected {
192				col: col.id(),
193				index,
194				index_bits: log2_strict_usize(VALUES_PER_ROW),
195			},
196		)
197	}
198
199	pub fn add_constant<FSub, const VALUES_PER_ROW: usize>(
200		&mut self,
201		name: impl ToString,
202		constants: [FSub; VALUES_PER_ROW],
203	) -> Col<FSub, VALUES_PER_ROW>
204	where
205		FSub: TowerField,
206		F: ExtensionField<FSub>,
207		OptimalUnderlier: PackScalar<FSub> + PackScalar<F>,
208	{
209		let namespaced_name = self.namespaced_name(name);
210		let n_vars = log2_strict_usize(VALUES_PER_ROW);
211		let packed_values: Vec<PackedType<OptimalUnderlier, FSub>> = pack_slice(&constants);
212		let mle = MultilinearExtensionTransparent::<
213			PackedType<OptimalUnderlier, FSub>,
214			PackedType<OptimalUnderlier, F>,
215			_,
216		>::from_values_and_mu(packed_values, n_vars)
217		.unwrap();
218		self.table.new_column(
219			namespaced_name,
220			ColumnDef::Constant {
221				poly: Arc::new(mle),
222			},
223		)
224	}
225
226	pub fn assert_zero<FSub, const VALUES_PER_ROW: usize>(
227		&mut self,
228		name: impl ToString,
229		expr: Expr<FSub, VALUES_PER_ROW>,
230	) where
231		FSub: TowerField,
232		F: ExtensionField<FSub>,
233	{
234		self.table
235			.partition_mut(VALUES_PER_ROW)
236			.assert_zero(name, expr)
237	}
238
239	/// Constrains that all values contained in this column are non-zero.
240	pub fn assert_nonzero<FSub, const V: usize>(&mut self, expr: Col<FSub, V>)
241	where
242		FSub: TowerField,
243		F: ExtensionField<FSub>,
244	{
245		assert_eq!(expr.table_id, self.id());
246		assert!(expr.table_index < self.table.columns.len());
247
248		self.table.columns[expr.table_index].is_nonzero = true;
249	}
250
251	pub fn pull<FSub>(&mut self, channel: ChannelId, cols: impl IntoIterator<Item = Col<FSub>>)
252	where
253		FSub: TowerField,
254		F: ExtensionField<FSub>,
255	{
256		self.pull_with_opts(channel, cols, FlushOpts::default());
257	}
258
259	pub fn push<FSub>(&mut self, channel: ChannelId, cols: impl IntoIterator<Item = Col<FSub>>)
260	where
261		FSub: TowerField,
262		F: ExtensionField<FSub>,
263	{
264		self.push_with_opts(channel, cols, FlushOpts::default());
265	}
266
267	pub fn pull_with_opts<FSub>(
268		&mut self,
269		channel: ChannelId,
270		cols: impl IntoIterator<Item = Col<FSub>>,
271		opts: FlushOpts,
272	) where
273		FSub: TowerField,
274		F: ExtensionField<FSub>,
275	{
276		self.table.partition_mut(1).flush(
277			channel,
278			FlushDirection::Pull,
279			cols.into_iter().map(upcast_col),
280			opts,
281		);
282	}
283
284	pub fn push_with_opts<FSub>(
285		&mut self,
286		channel: ChannelId,
287		cols: impl IntoIterator<Item = Col<FSub>>,
288		opts: FlushOpts,
289	) where
290		FSub: TowerField,
291		F: ExtensionField<FSub>,
292	{
293		self.table.partition_mut(1).flush(
294			channel,
295			FlushDirection::Push,
296			cols.into_iter().map(upcast_col),
297			opts,
298		);
299	}
300
301	fn namespaced_name(&self, name: impl ToString) -> String {
302		let name = name.to_string();
303		match &self.namespace {
304			Some(namespace) => format!("{namespace}::{name}"),
305			None => name.to_string(),
306		}
307	}
308}
309
310/// A table in an M3 constraint system.
311///
312/// ## Invariants
313///
314/// * All expressions in `zero_constraints` have a number of variables less than or equal to the
315///   number of table columns (the length of `column_info`).
316/// * All flushes in `flushes` contain column indices less than the number of table columns (the
317///   length of `column_info`).
318#[derive(Debug)]
319pub struct Table<F: TowerField = B128> {
320	pub id: TableId,
321	pub name: String,
322	pub columns: Vec<ColumnInfo<F>>,
323	pub(super) partitions: SparseIndex<TablePartition<F>>,
324}
325
326/// A table partition describes a part of a table where everything has the same pack factor (as well as height)
327/// Tower level does not need to be the same.
328///
329/// Zerocheck constraints can only be defined within table partitions.
330#[derive(Debug)]
331pub(super) struct TablePartition<F: TowerField = B128> {
332	pub table_id: TableId,
333	pub values_per_row: usize,
334	pub flushes: Vec<Flush>,
335	pub columns: Vec<ColumnIndex>,
336	pub zero_constraints: Vec<ZeroConstraint<F>>,
337}
338
339impl<F: TowerField> TablePartition<F> {
340	fn new(table_id: TableId, values_per_row: usize) -> Self {
341		Self {
342			table_id,
343			values_per_row,
344			flushes: Vec::new(),
345			columns: Vec::new(),
346			zero_constraints: Vec::new(),
347		}
348	}
349
350	fn assert_zero<FSub, const VALUES_PER_ROW: usize>(
351		&mut self,
352		name: impl ToString,
353		expr: Expr<FSub, VALUES_PER_ROW>,
354	) where
355		FSub: TowerField,
356		F: ExtensionField<FSub>,
357	{
358		// TODO: Should we dynamically keep track of FSub::TOWER_LEVEL?
359		// On the other hand, ArithExpr does introspect that already
360		self.zero_constraints.push(ZeroConstraint {
361			name: name.to_string(),
362			expr: expr.expr().convert_field(),
363		});
364	}
365
366	fn flush(
367		&mut self,
368		channel_id: ChannelId,
369		direction: FlushDirection,
370		cols: impl IntoIterator<Item = Col<F>>,
371		opts: FlushOpts,
372	) {
373		let column_indices = cols
374			.into_iter()
375			.map(|col| {
376				assert_eq!(col.table_id, self.table_id);
377				col.table_index
378			})
379			.collect();
380		let selector = opts.selector.map(|selector| {
381			assert_eq!(selector.table_id, self.table_id);
382			selector.table_index
383		});
384		self.flushes.push(Flush {
385			column_indices,
386			channel_id,
387			direction,
388			multiplicity: opts.multiplicity,
389			selector,
390		});
391	}
392}
393
394impl<F: TowerField> Table<F> {
395	pub fn new(id: TableId, name: impl ToString) -> Self {
396		Self {
397			id,
398			name: name.to_string(),
399			columns: Vec::new(),
400			partitions: SparseIndex::new(),
401		}
402	}
403
404	pub fn id(&self) -> TableId {
405		self.id
406	}
407
408	/// Returns the binary logarithm of the minimum capacity.
409	///
410	/// This value is chosen so that every committed column fills at least one large field element
411	/// in packed representation. This is because the polynomial commitment scheme requires full
412	/// packed field elements.
413	pub fn min_log_capacity(&self) -> usize {
414		let min_cell_size = self
415			.columns
416			.iter()
417			.filter_map(|col| match col.col {
418				ColumnDef::Committed { .. } => Some(col.shape.log_cell_size()),
419				_ => None,
420			})
421			.min()
422			// return 0 if table has no columns
423			.unwrap_or(F::TOWER_LEVEL);
424		F::TOWER_LEVEL.saturating_sub(min_cell_size)
425	}
426
427	/// Returns the binary logarithm of the table capacity required to accommodate the given number
428	/// of rows.
429	///
430	/// The table capacity must be a power of two (in order to be compatible with the multilinear
431	/// proof system, which associates each table index with a vertex of a boolean hypercube).
432	/// This will normally be the next power of two greater than the table size, but could require
433	/// more padding to get a minimum capacity.
434	pub fn log_capacity(&self, table_size: usize) -> usize {
435		log2_ceil_usize(table_size).max(self.min_log_capacity())
436	}
437
438	fn new_column<FSub, const V: usize>(
439		&mut self,
440		name: impl ToString,
441		col: ColumnDef<F>,
442	) -> Col<FSub, V>
443	where
444		FSub: TowerField,
445		F: ExtensionField<FSub>,
446	{
447		let table_id = self.id;
448		let table_index = self.columns.len();
449		let partition = self.partition_mut(V);
450		let id = ColumnId {
451			table_id,
452			table_index,
453		};
454		let info = ColumnInfo {
455			id,
456			col,
457			name: name.to_string(),
458			shape: ColumnShape {
459				tower_height: FSub::TOWER_LEVEL,
460				log_values_per_row: log2_strict_usize(V),
461			},
462			is_nonzero: false,
463		};
464
465		let partition_index = partition.columns.len();
466		partition.columns.push(table_index);
467		self.columns.push(info);
468		Col::new(id, partition_index)
469	}
470
471	fn partition_mut(&mut self, values_per_row: usize) -> &mut TablePartition<F> {
472		self.partitions
473			.entry(log2_strict_usize(values_per_row))
474			.or_insert_with(|| TablePartition::new(self.id, values_per_row))
475	}
476}
477
478const fn partition_id<const V: usize>() -> usize {
479	checked_log_2(V)
480}