38std::string make_symbol_string(std::string_view prefix, std::uint32_t index, std::string_view suffix = {}) {
39 std::array<char, std::numeric_limits<std::uint32_t>::digits10 + 1> digits{};
40 const auto [ptr, ec] = std::to_chars(digits.data(), digits.data() + digits.size(), index);
41 assert(ec == std::errc() &&
"uint32_t index must format into a fixed-size decimal buffer");
44 out.reserve(prefix.size() +
static_cast<std::size_t
>(ptr - digits.data()) + suffix.size());
46 out.append(digits.data(), ptr);
78 return make_symbol_string(
"v[",
index,
"]");
80 return make_symbol_string(
"L",
index);
82 return make_symbol_string(
"R",
index);
84 return make_symbol_string(
"O",
index);
86 return make_symbol_string(
"V",
index);
88 assert(
false &&
"unknown SymbolKind");
105 std::ostringstream out;
107 if (!constant_.
is_zero() || linear_.empty()) {
111 for (
const auto& term : linear_) {
116 out << term.first.to_string();
118 out << term.second.to_decimal() <<
" * " << term.first.to_string();
127 for (
const auto& term : linear_) {
131 std::size_t index = term.first.index;
132 if (index >= values.size() || !values[index].has_value()) {
135 out = out + (*values[index] * term.second);
142 linear_expr.linear_ = linear_;
143 return {
Expr(constant_), linear_expr};
146void Expr::push_term(
const Term& term) {
147 if (term.second.is_zero()) {
150 if (!linear_.empty() && linear_.back().first == term.first) {
151 linear_.back().second = linear_.back().second + term.second;
152 if (linear_.back().second.is_zero()) {
157 linear_.push_back(term);
162 builder.terms_.reserve(terms);
167 terms_.reserve(terms);
172 constant_ = constant_ + value;
182 terms_.push_back({symbol, scale});
188 constant_ = constant_ + expr.
constant();
189 if (expr.
linear().empty()) {
192 terms_.reserve(terms_.size() + expr.
linear().size());
193 for (
const auto& term : expr.
linear()) {
194 terms_.push_back(term);
200 constant_ = constant_ - expr.
constant();
201 if (expr.
linear().empty()) {
204 terms_.reserve(terms_.size() + expr.
linear().size());
205 for (
const auto& term : expr.
linear()) {
208 terms_.push_back({term.first, coeff});
224 constant_ = constant_ + expr.
constant() * scale;
225 if (expr.
linear().empty()) {
228 terms_.reserve(terms_.size() + expr.
linear().size());
229 for (
const auto& term : expr.
linear()) {
232 terms_.push_back({term.first, coeff});
244 if (terms_.empty()) {
248 return SymbolLess{}(lhs.first, rhs.first);
250 auto& linear = out.
linear();
251 linear.reserve(terms_.size());
252 for (
const auto& term : terms_) {
253 if (!linear.empty() && linear.back().first == term.first) {
254 linear.back().second = linear.back().second + term.second;
255 if (linear.back().second.is_zero()) {
258 }
else if (!term.second.is_zero()) {
259 linear.push_back(term);
266 Expr out(lhs.constant_ + rhs.constant_);
267 out.linear_.reserve(lhs.linear_.size() + rhs.linear_.size());
271 while (i < lhs.linear_.size() || j < rhs.linear_.size()) {
272 if (j == rhs.linear_.size() || (i < lhs.linear_.size() && less(lhs.linear_[i].first, rhs.linear_[j].first))) {
273 out.push_term(lhs.linear_[i]);
275 }
else if (i == lhs.linear_.size() || less(rhs.linear_[j].first, lhs.linear_[i].first)) {
276 out.push_term(rhs.linear_[j]);
279 out.push_term({lhs.linear_[i].first, lhs.linear_[i].second + rhs.linear_[j].second});
288 return lhs +
Expr(rhs);
292 return Expr(lhs) + rhs;
300 return lhs -
Expr(rhs);
304 return Expr(lhs) - rhs;
308 return value * FieldElement::from_int(-1);
316 out.linear_.reserve(expr.linear_.size());
317 for (
const auto& term : expr.linear_) {
318 out.linear_.push_back({term.first, term.second *
scalar});
328 return expr * FieldElement::from_int(
scalar);
336 return lhs.constant_ == rhs.constant_ && lhs.linear_ == rhs.linear_;
339bool ExprLess::operator()(
const Expr& lhs,
const Expr& rhs)
const {
341 if (constant_cmp != 0) {
342 return constant_cmp < 0;
344 std::size_t common = std::min(lhs.
linear().size(), rhs.
linear().size());
345 for (std::size_t i = 0; i < common; ++i) {
346 int symbol_cmp = compare_symbols(lhs.
linear()[i].first, rhs.
linear()[i].first);
347 if (symbol_cmp != 0) {
348 return symbol_cmp < 0;
350 int coeff_cmp = compare_field_elements(lhs.
linear()[i].second, rhs.
linear()[i].second);
351 if (coeff_cmp != 0) {
352 return coeff_cmp < 0;
358bool ExprPairLess::operator()(
const std::pair<Expr, Expr>& lhs,
const std::pair<Expr, Expr>& rhs)
const {
360 if (less(lhs.first, rhs.first)) {
363 if (less(rhs.first, lhs.first)) {
366 return less(lhs.second, rhs.second);
378Expr Transcript::secret(
const std::optional<FieldElement>& value) {
379 std::size_t index = varmap_.size();
380 assert(index <=
static_cast<std::size_t
>(std::numeric_limits<std::uint32_t>::max())
381 &&
"Transcript::secret() witness index must fit in uint32_t");
382 varmap_.push_back(value);
383 return Expr::variable(Symbol::witness(
static_cast<std::uint32_t
>(index)));
387 auto direct = std::make_pair(lhs, rhs);
388 auto reverse = std::make_pair(rhs, lhs);
389 auto it = mul_cache_.find(direct);
390 if (it != mul_cache_.end()) {
393 it = mul_cache_.find(reverse);
394 if (it != mul_cache_.end()) {
397 std::optional<FieldElement> lhs_val = lhs.
evaluate(varmap_);
398 std::optional<FieldElement> rhs_val = rhs.
evaluate(varmap_);
399 std::optional<FieldElement> value;
400 if (lhs_val.has_value() && rhs_val.has_value()) {
401 value = *lhs_val * *rhs_val;
403 Expr out = secret(value);
404 mul_cache_[direct] = out;
405 muls_.push_back({lhs, rhs, out});
410 auto direct = std::make_pair(lhs, rhs);
411 auto it = div_cache_.find(direct);
412 if (it != div_cache_.end()) {
415 std::optional<FieldElement> lhs_val = lhs.
evaluate(varmap_);
416 std::optional<FieldElement> rhs_val = rhs.
evaluate(varmap_);
417 assert((!rhs_val.has_value() || !rhs_val->is_zero()) &&
"Transcript::div() requires a non-zero known divisor");
418 std::optional<FieldElement> value;
419 if (lhs_val.has_value() && rhs_val.has_value()) {
420 value = *lhs_val * rhs_val->inverse();
422 Expr out = secret(value);
423 div_cache_[direct] = out;
424 muls_.push_back({out, rhs, lhs});
429 if (bool_cache_.count(expr) != 0) {
433 std::optional<FieldElement> value = expr.
evaluate(varmap_);
434 assert((!value.has_value() || *value == FieldElement::zero() || *value == FieldElement::one())
435 &&
"Transcript::boolean() requires a known value to be 0 or 1");
437 bool_cache_.insert(expr);
438 muls_.push_back({expr, expr - 1,
Expr(0)});
442void Transcript::equal(
const Expr& lhs,
const Expr& rhs) {
443 Expr diff = lhs - rhs;
445 std::optional<FieldElement> value = diff.
evaluate(varmap_);
446 assert((!value.has_value() || value->is_zero()) &&
"Transcript::equal() requires known values to match");
448 eqs_.push_back(diff);
451std::optional<FieldElement> Transcript::evaluate(
const Expr& expr)
const {
Small runtime builder that flattens affine combinations into one expression.
ExprBuilder & add(const FieldElement &value)
Adds a constant field term to the pending affine expression.
ExprBuilder & add_term(Symbol symbol, const FieldElement &scale)
Adds one scaled symbolic variable term.
static ExprBuilder reserved(std::size_t terms)
Returns a builder with storage reserved for approximately terms linear slots.
Expr build()
Materializes the flattened affine expression.
ExprBuilder & add_scaled(const Expr &expr, const FieldElement &scale)
Adds an existing expression scaled by a field element.
ExprBuilder & subtract(const Expr &expr)
Subtracts an existing expression with implicit coefficient minus one.
ExprBuilder & reserve(std::size_t terms)
Reserves storage for approximately terms linear slots.
Symbolic affine expression over indexed variables and field coefficients.
const FieldElement & constant() const
Returns the constant term of the affine expression.
std::string to_string() const
Formats the expression in a stable human-readable form used for debugging and serialization.
std::vector< Term > & linear()
Returns mutable access to the sorted linear term list.
Expr()
Constructs the zero expression.
std::pair< Symbol, FieldElement > Term
static Expr variable(Symbol symbol)
Returns a single-variable expression with coefficient one.
std::optional< FieldElement > evaluate(const WitnessAssignments &values) const
Evaluates the expression against a possibly partial transcript witness assignment.
std::pair< Expr, Expr > split() const
Splits the expression into a pure constant and a pure linear component.
Field element modulo the backend scalar field used by this implementation.
bool is_one() const
Returns true when the element is one.
std::string to_decimal() const
Formats the field element as an unsigned decimal string.
static FieldElement one()
Returns the multiplicative identity of the scalar field.
FieldElement negate() const
Returns the additive inverse modulo the field prime.
static FieldElement from_int(std::int64_t value)
Constructs a field element from a signed integer, reducing negatives modulo the field.
UInt256 to_uint256() const
Exports the field element as a canonical 256-bit unsigned integer.
bool is_zero() const
Returns true when the element is zero.
Symbolic expression and transcript machinery used to derive Purify circuits.
bool operator<(const Symbol &lhs, const Symbol &rhs) noexcept
Expr operator*(const Expr &expr, const FieldElement &scalar)
std::ostream & operator<<(std::ostream &out, const Expr &expr)
Streams the human-readable expression form to an output stream.
bool operator==(const Expr &lhs, const Expr &rhs)
Expr operator-(const Expr &lhs, const Expr &rhs)
std::vector< std::optional< FieldElement > > WitnessAssignments
Partial witness assignment vector indexed by transcript witness id.
Bytes operator+(Bytes lhs, const Bytes &rhs)
Concatenates two byte vectors.
int compare(const BigUInt &other) const
Compares two fixed-width integers using unsigned ordering.
Compact symbolic variable identifier used inside expressions and transcripts.
static Symbol witness(std::uint32_t index)
static Symbol left(std::uint32_t index)
static Symbol output(std::uint32_t index)
std::string to_string() const
static Symbol commitment(std::uint32_t index)
static Symbol right(std::uint32_t index)