Skip to content

Poly lowering rebuilds nested member-read auxiliary expressions from the wrong component #491

Description

@1sgtpepper

Poly lowering rebuilds nested member-read auxiliary expressions from the wrong component

Summary

When polynomial lowering introduces an auxiliary member for an expression involving a nested member read, rebuildExprInCompute rebuilds every struct.readm from the enclosing compute function's %self. It does not rebuild the original read's component operand. A constrain-side path like parent.child.val can therefore become parent.val in compute().

Root cause

rebuildExprInCompute handles MemberReadOp by creating a new read from computeFunc.getSelfValueFromCompute() and the same member name. It should recursively rebuild readOp.getComponent() instead.

The helper is used when poly lowering writes generated auxiliary members here, and the R1CS lowering path uses the same helper for aux writes here.

Reproduction

Use a parent and child that both have a compatible @val member:

module attributes {llzk.lang} {
  struct.def @Child {
    struct.member @val : !felt.type {llzk.pub}
    function.def @compute(%x: !felt.type) -> !struct.type<@Child> {
      %self = struct.new : !struct.type<@Child>
      struct.writem %self[@val] = %x : !struct.type<@Child>, !felt.type
      function.return %self : !struct.type<@Child>
    }
  }
  struct.def @Parent {
    struct.member @child : !struct.type<@Child>
    struct.member @val : !felt.type
    function.def @compute(%x: !felt.type) -> !struct.type<@Parent> {
      %self = struct.new : !struct.type<@Parent>
      function.return %self : !struct.type<@Parent>
    }
    function.def @constrain(%self: !struct.type<@Parent>, %x: !felt.type) {
      %child = struct.readm %self[@child] : !struct.type<@Parent>, !struct.type<@Child>
      %child_val = struct.readm %child[@val] : !struct.type<@Child>, !felt.type
      %sq = felt.mul %child_val, %child_val
      %quartic = felt.mul %sq, %sq
      %parent_val = struct.readm %self[@val] : !struct.type<@Parent>, !felt.type
      constrain.eq %parent_val, %quartic : !felt.type
      function.return
    }
  }
}

Run:

llzk-opt -I . -split-input-file -llzk-full-poly-lowering nested_member_read_aux_rebuild.llzk

Observed output from llzk-opt 2.0.0 at 22931df604ba40ecf50c6260c90365df16a9c222:

function.def @compute(%arg0: !felt.type) -> !struct.type<@Parent> {
  %self = struct.new : <@Parent>
  %0 = struct.readm %self[@val] : <@Parent>, !felt.type
  %1 = felt.mul %0, %0 : !felt.type, !felt.type
  struct.writem %self[@__llzk_poly_lowering_pass_aux_member_0] = %1 : <@Parent>, !felt.type
  function.return %self : !struct.type<@Parent>
}

The lowered constrain() still reads @child and then child @val; only the generated compute-side aux assignment uses direct parent @val.

Expected behavior

The compute-side auxiliary expression should preserve the member path shape: read %self[@child], then read @val from that child, then compute the auxiliary value.

Impact

The generated witness computation can disagree with the lowered constraints. In this example, the aux value is computed from Parent.val^2, while the constrain-side relation expects it to correspond to Child.val^2. This changes the semantics of lowered LLZK programs that use nested component fields in high-degree expressions.

Fix direction

In rebuildExprInCompute, map the original constrain %self to compute %self, then recursively rebuild the original MemberReadOp component before recreating the read. Do not replace every member-read base with the enclosing compute %self.

Regression test

Add a poly-lowering test with both Parent.@val and Child.@val. FileCheck should require a compute-side struct.readm %self[@child] followed by struct.readm <child>[@val], and reject computing the aux from direct Parent.@val.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions