1 - Contributing to HEIR

There are several ways to contribute to HEIR, including:

Ways to contribute

We welcome pull requests, and have tagged issues for newcomers:

For new proposals, please open a GitHub issue or start a discussion for feedback.

Contributing to code using pull requests

Preparing a pull request

The following steps should look familiar to typical workflows for pull request contributions. Feel free to consult GitHub Help if you need more information using pull requests. HEIR-specific processes begin at the pull request review stage.

  1. Sign the Contributor License Agreement (CLA). See more here.

  2. Fork the HEIR repository by clicking the Fork button on the repository page. This creates a copy of the HEIR repository on your own GitHub account.

  3. See Getting Started to install developer dependencies to build and run tests.

  4. Add the HEIR repository as an upstream remote, so you can sync your changes against it.

    git remote add upstream https://www.github.com/google/heir
    
  5. Create a development branch for your change:

     git checkout -b name-of-change
    

    And implement your changes using your favorite IDE. See IDE Configuration for more.

  6. Check HEIR’s lint and style checks by running the following from the top of the repository:

     pre-commit run --all
    
  7. Make sure tests are passing with the following:

     bazel test @heir//...
    
  8. Once you are ready with your change, create a commit as follows.

    git add change.cpp
    git commit -m "Detailed commit message"
    git push --set-upstream origin name-of-change
    

Pull request review flow

  1. New PR:
  • When a new PR is submitted, it is inspected for quality requirements, such as the CLA requirement, and a sufficient PR description.
  • If the PR passes checks, we assign a reviewer. If not, we request additional changes to ensure the PR passes CI checks.
  1. Review
  • A reviewer will check the PR and potentially request additional changes.
  • If a change is needed, the contributor is requested to make a suggested change. Please make changes with additional commits to your PR, to ensure that the reviewer can easily see the diff.
  • If all looks good, the reviewer will approve the PR.
  • This cycle repeats itself until the PR is approved.
  1. Approved
  • At this stage, you must squash your commits into a single commit.
  • Once the PR is approved, a GitHub workflow will check your PR for multiple commits. You may use the git rebase -i to squash the commits. Pull requests must comprise of a single git commit before merging.
  1. Pull Ready
  • Once the PR is squashed into a single git commit, a maintainer will apply the pull ready label.
  • This initiates the internal code migration and presubmits.
  • After the internal process is finished, the commit will be added to main and the PR closed as merged by that commit.

Internal review details

This diagram summarizes the GitHub/Google code synchronization process. This is largely automated by a Google-owned system called Copybara, the configuration for which is Google-internal. This system treats the Google-internal version of HEIR as the source of truth, and applies specified transformation rules to copy internal changes to GitHub and integrate external PRs internally.

Notable aspects:

  • The final merged code may differ slightly from a PR. The changes are mainly to support stricter internal requirements for BUILD files that we cannot reproduce externally due to minor differences between Google’s internal build systems and bazel that we don’t know how to align. Sometimes they will also include additional code quality fixes suggested by internal static analyzers that do not exist outside of Google.
  • Due to the above, signed commits with internal modifications will not maintain valid signatures after merging, which labels the commit with a warning.
  • You will see various actions taken on GitHub that include copybara in the name, such as changes that originate from Google engineers doing various approved migrations (e.g., migrating HEIR to support changes in MLIR or abseil).

A diagram summarizing the copybara flow for HEIR internally to Google

Why bother with Copybara?

tl;dr: Automatic syncing with upstream MLIR and associated code migration.

Until HEIR has a formal governance structure in place, Google engineers—specifically Asra Ali, Shruthi Gorantala, and Jeremy Kun—are the codebase stewards. Because the project is young and the team is small, we want to reduce our workload. One important aspect of that is keeping up to date with the upstream MLIR project and incorporating bug fixes and new features into HEIR. Google also wishes to stay up to date with MLIR and LLVM, and so it has tooling devoted to integrating new MLIR changes into Google’s monorepo every few hours. As part of that rotation, a set of approved internal projects that depend on MLIR (like TensorFlow) are patched to support breaking changes in MLIR. HEIR is one of those approved projects.

As shown in the previous section, the cost of this is that no change can go into HEIR without at least two Googlers approving it, and the project is held to a specific set of code quality standards, namely Google’s. We acknowledge these quirks, and look forward to the day when HEIR is useful enough and important enough that we can revisit this governance structure with the community.

2 - IDE Configuration

VS Code

For an out-of-tree MLIR project built with Bazel, install the following VS Code extensions:

  • llvm-vs-code-extensions.vscode-mlir: Adds language support for MLIR, PDLL, and TableGen.
  • llvm-vs-code-extensions.vscode-clangd: Adds clangd code completion using a generated compile_commands.json file.
  • bazelbuild.vscode-bazel: Support for Bazel.

You will also need to disable ms-vscode.cpptools to avoid a conflict with clangd.

Add the following snippet to your VS Code user settings found in .vscode/settings.json to enable autocomplete based on the compile_commands.json file.

   "clangd.arguments": [
        "--compile-commands-dir=${workspaceFolder}/",
        "--completion-style=detailed",
        "--query-driver=**"
      ],

To generate the compile_commands.json file, run

bazel run @hedron_compile_commands//:refresh_all

This will need to be regenerated every time you want tooling to see new BUILD file changes.

If you encounter errors like *.h.inc not found, or syntax errors inside these files, you may need to build those targets and then re-run the refresh_all command above.

Tips for working with Bazel

Avoiding rebuilds

Bazel is notoriously fickle when it comes to deciding whether a full rebuild is necessary, which is bad for HEIR because rebuilding LLVM from scratch takes 15 minutes or more.

The main things that cause a rebuild are:

  • A change to the command-line flags passed to bazel, e.g., -c opt vs -c dbg for optimization level and debug symbols.
  • A change to the .bazelrc that implicitly causes a flag change. Note HEIR has its own project-specific .bazelrc in the root directory.
  • A change to relevant command-line variables, such as PATH, which is avoided by the incompatible_strict_action_env flag. Note activating a python virtualenv triggers a PATH change.

Bazel compilation flags are set by default in the project root’s .bazelrc in such a way as to avoid rebuilds during development as much as possible. This includes setting -c dbg and --incompatible_strict_action_env.

Pointing HEIR to a local clone of llvm-project

Occasionally changes in HEIR will need to be made in tandem with upstream changes in MLIR. In particular, we occasionally find upstream bugs that only occur with HEIR passes, and we are the primary owners/users of the upstream polynomial dialect.

To tell bazel to use a local clone of llvm-project instead of a pinned commit hash, replace bazel/import_llvm.bzl with the following file:

cat > bazel/import_llvm.bzl << EOF
"""Provides the repository macro to import LLVM."""

def import_llvm(name):
    """Imports LLVM."""
    native.new_local_repository(
        name = name,
        # this BUILD file is intentionally empty, because the LLVM project
        # internally contains a set of bazel BUILD files overlaying the project.
        build_file_content = "# empty",
        path = "/path/to/llvm-project",
    )
EOF

The next bazel build will require a full rebuild if the checked-out LLVM commit differs from the pinned commit hash in bazel/import_llvm.bzl.

Note that you cannot reuse the LLVM CMake build artifacts in the bazel build. Based on what you’re trying to do, this may require some extra steps.

  • If you just want to run existing MLIR and HEIR tests against local llvm-project changes, you can run the tests from HEIR using bazel test @llvm-project//mlir/...:all. New lit tests can be added in llvm-project’s existing directories and tested this way without a rebuild.
  • If you add new CMake targets in llvm-project, then to incorporate them into HEIR you need to add new bazel targets in llvm-project/utils/bazel/llvm-project-overlay/mlir/BUILD.bazel. This is required if, for example, a new dialect or pass is added in MLIR upstream.

Send any upstream changes to HEIR-relevant MLIR files to @j2kun (Jeremy Kun) who has LLVM commit access and can also suggest additional MLIR reviewers.

3 - Dialects

This section contains the reference documentation for all of the dialects defined in HEIR.

3.1 - ArithExt

ArithExt ops

arith_ext.add (heir::arith_ext::AddOp)

Modular addition operation

Syntax:

operation ::= `arith_ext.add` operands attr-dict `:` type($output)

Computes addition modulo a statically known modulus $q$.

Traits: AlwaysSpeculatableImplTrait, Commutative, Elementwise, SameOperandsAndResultType, Scalarizable, Tensorizable, Vectorizable

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
modulus::mlir::IntegerAttrarbitrary integer attribute

Operands:

OperandDescription
lhssignless-integer-like
rhssignless-integer-like

Results:

ResultDescription
outputsignless-integer-like

arith_ext.barrett_reduce (heir::arith_ext::BarrettReduceOp)

Compute the first step of the Barrett reduction.

Syntax:

operation ::= `arith_ext.barrett_reduce` operands attr-dict `:` qualified(type($input))

Let $q$ denote a statically known modulus and $b = 4^{w}$, where $w$ is the smallest bit-width that contains the range $[0, q)$. The Barrett reduce operation computes barret_reduce x = x - floor(x * floor(b / q) / b) * q.

Given $0 <= x < q^2$, then this will compute $(x \mod q)$ or $(x \mod q) + p$.

Traits: SameOperandsAndResultType

Interfaces: InferTypeOpInterface

Attributes:

AttributeMLIR TypeDescription
modulus::mlir::IntegerAttrarbitrary integer attribute

Operands:

OperandDescription
inputsignless-integer-like

Results:

ResultDescription
outputsignless-integer-like

arith_ext.mac (heir::arith_ext::MacOp)

Modular multiplication-and-accumulation operation

Syntax:

operation ::= `arith_ext.mac` operands attr-dict `:` type($output)

arith_ext.mac x, y, z {modulus = q} computes $(x * y) + z \mod q$

Traits: AlwaysSpeculatableImplTrait, Elementwise, SameOperandsAndResultType, Scalarizable, Tensorizable, Vectorizable

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
modulus::mlir::IntegerAttrarbitrary integer attribute

Operands:

OperandDescription
lhssignless-integer-like
rhssignless-integer-like
accsignless-integer-like

Results:

ResultDescription
outputsignless-integer-like

arith_ext.mul (heir::arith_ext::MulOp)

Modular multiplication operation

Syntax:

operation ::= `arith_ext.mul` operands attr-dict `:` type($output)

Computes multiplication modulo a statically known modulus $q$.

Traits: AlwaysSpeculatableImplTrait, Commutative, Elementwise, SameOperandsAndResultType, Scalarizable, Tensorizable, Vectorizable

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
modulus::mlir::IntegerAttrarbitrary integer attribute

Operands:

OperandDescription
lhssignless-integer-like
rhssignless-integer-like

Results:

ResultDescription
outputsignless-integer-like

arith_ext.subifge (heir::arith_ext::SubIfGEOp)

Compute (x >= y) ? x - y : x.

Syntax:

operation ::= `arith_ext.subifge` operands attr-dict `:` qualified(type($output))

Traits: SameOperandsAndResultType

Interfaces: InferTypeOpInterface

Operands:

OperandDescription
lhssignless-integer-like
rhssignless-integer-like

Results:

ResultDescription
outputsignless-integer-like

arith_ext.sub (heir::arith_ext::SubOp)

Modular subtraction operation

Syntax:

operation ::= `arith_ext.sub` operands attr-dict `:` type($output)

Computes subtraction modulo a statically known modulus $q$.

Traits: AlwaysSpeculatableImplTrait, Elementwise, SameOperandsAndResultType, Scalarizable, Tensorizable, Vectorizable

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
modulus::mlir::IntegerAttrarbitrary integer attribute

Operands:

OperandDescription
lhssignless-integer-like
rhssignless-integer-like

Results:

ResultDescription
outputsignless-integer-like

3.2 - BGV

The BGV dialect defines the types and operations of the BGV cryptosystem.

BGV ops

bgv.add (heir::bgv::AddOp)

Addition operation between ciphertexts.

Syntax:

operation ::= `bgv.add` operands attr-dict `:` qualified(type($output))

Traits: Commutative, SameOperandsAndResultType

Interfaces: InferTypeOpInterface

Operands:

OperandDescription
lhsA type for RLWE ciphertexts
rhsA type for RLWE ciphertexts

Results:

ResultDescription
outputA type for RLWE ciphertexts

bgv.add_plain (heir::bgv::AddPlainOp)

Addition operation between ciphertext-plaintext.

Syntax:

operation ::= `bgv.add_plain` operands attr-dict `:`  functional-type(operands, results)

Interfaces: InferTypeOpInterface

Operands:

OperandDescription
ciphertextInputA type for RLWE ciphertexts
plaintextInputA type for RLWE plaintexts

Results:

ResultDescription
outputA type for RLWE ciphertexts

bgv.extract (heir::bgv::ExtractOp)

Extract the i-th element of a ciphertext.

Syntax:

operation ::= `bgv.extract` operands attr-dict `:`  functional-type(operands, results)

While this operation is costly to compute in FHE, we represent it so we can implement efficient lowerings and folders.

This op can be implemented as a plaintext multiplication with a one-hot vector and a rotate into the zero-th index.

An extraction op’s input ciphertext type is asserted to have an underlying_type corresponding to a ranked tensor type, and this op’s return type is inferred to have the underlying_type corresponding to the element type of that tensor type.

Traits: SameOperandsAndResultRings

Operands:

OperandDescription
inputA type for RLWE ciphertexts
offsetsignless integer or index

Results:

ResultDescription
outputA type for RLWE ciphertexts

bgv.modulus_switch (heir::bgv::ModulusSwitchOp)

Lower the modulus level of the ciphertext.

Syntax:

operation ::= `bgv.modulus_switch` operands attr-dict `:` qualified(type($input)) `->` qualified(type($output))

Attributes:

AttributeMLIR TypeDescription
to_ring::mlir::polynomial::RingAttran attribute specifying a polynomial ring

Operands:

OperandDescription
inputA type for RLWE ciphertexts

Results:

ResultDescription
outputA type for RLWE ciphertexts

bgv.mul (heir::bgv::MulOp)

Multiplication operation between ciphertexts.

Syntax:

operation ::= `bgv.mul` operands attr-dict `:`  functional-type(operands, results)

Traits: Commutative, InferTypeOpAdaptor, SameOperandsAndResultRings, SameTypeOperands

Interfaces: InferTypeOpInterface

Operands:

OperandDescription
lhsA type for RLWE ciphertexts
rhsA type for RLWE ciphertexts

Results:

ResultDescription
outputA type for RLWE ciphertexts

bgv.mul_plain (heir::bgv::MulPlainOp)

Multiplication operation between ciphertext-plaintext.

Syntax:

operation ::= `bgv.mul_plain` operands attr-dict `:`  functional-type(operands, results)

Interfaces: InferTypeOpInterface

Operands:

OperandDescription
ciphertextInputA type for RLWE ciphertexts
plaintextInputA type for RLWE plaintexts

Results:

ResultDescription
outputA type for RLWE ciphertexts

bgv.negate (heir::bgv::NegateOp)

Negate the coefficients of the ciphertext.

Syntax:

operation ::= `bgv.negate` operands attr-dict `:` qualified(type($output))

Traits: SameOperandsAndResultType

Interfaces: InferTypeOpInterface

Operands:

OperandDescription
inputA type for RLWE ciphertexts

Results:

ResultDescription
outputA type for RLWE ciphertexts

bgv.relinearize (heir::bgv::RelinearizeOp)

Relinearize the ciphertext.

Syntax:

operation ::= `bgv.relinearize` operands attr-dict `:` qualified(type($input)) `->` qualified(type($output))

This op takes integer array attributes from_basis and to_basis that are used to indicate the key basis from which and to which the ciphertext is encrypted against. A ciphertext is canonically encrypted against key basis (1, s). After a multiplication, its size will increase and the basis will be (1, s, s^2). The array that represents the key basis is constructed by listing the powers of s at each position of the array. For example, (1, s, s^2) corresponds to [0, 1, 2], while (1, s^2) corresponds to [0, 2].

Traits: InferTypeOpAdaptor, SameOperandsAndResultRings

Interfaces: InferTypeOpInterface

Attributes:

AttributeMLIR TypeDescription
from_basis::mlir::DenseI32ArrayAttri32 dense array attribute
to_basis::mlir::DenseI32ArrayAttri32 dense array attribute

Operands:

OperandDescription
inputA type for RLWE ciphertexts

Results:

ResultDescription
outputA type for RLWE ciphertexts

bgv.rotate (heir::bgv::RotateOp)

Rotate the coefficients of the ciphertext using a Galois automorphism.

Syntax:

operation ::= `bgv.rotate` operands attr-dict `:` qualified(type($input))

Interfaces: InferTypeOpInterface

Attributes:

AttributeMLIR TypeDescription
offset::mlir::IntegerAttrAn Attribute containing a integer value

Operands:

OperandDescription
inputA type for RLWE ciphertexts

Results:

ResultDescription
outputA type for RLWE ciphertexts

bgv.sub (heir::bgv::SubOp)

Subtraction operation between ciphertexts.

Syntax:

operation ::= `bgv.sub` operands attr-dict `:` qualified(type($output))

Traits: SameOperandsAndResultType

Interfaces: InferTypeOpInterface

Operands:

OperandDescription
lhsA type for RLWE ciphertexts
rhsA type for RLWE ciphertexts

Results:

ResultDescription
outputA type for RLWE ciphertexts

bgv.sub_plain (heir::bgv::SubPlainOp)

Subtraction operation between ciphertext-plaintext.

Syntax:

operation ::= `bgv.sub_plain` operands attr-dict `:`  functional-type(operands, results)

Interfaces: InferTypeOpInterface

Operands:

OperandDescription
ciphertextInputA type for RLWE ciphertexts
plaintextInputA type for RLWE plaintexts

Results:

ResultDescription
outputA type for RLWE ciphertexts

3.3 - CGGI

A dialect for types and operations in the CGGI cryptosystem

CGGI attributes

CGGIParamsAttr

Syntax:

#cggi.cggi_params<
  ::mlir::heir::lwe::RLWEParamsAttr,   # rlweParams
  unsigned,   # bsk_noise_variance
  unsigned,   # bsk_gadget_base_log
  unsigned,   # bsk_gadget_num_levels
  unsigned,   # ksk_noise_variance
  unsigned,   # ksk_gadget_base_log
  unsigned   # ksk_gadget_num_levels
>

Parameters:

ParameterC++ typeDescription
rlweParams::mlir::heir::lwe::RLWEParamsAttr
bsk_noise_varianceunsigned
bsk_gadget_base_logunsigned
bsk_gadget_num_levelsunsigned
ksk_noise_varianceunsigned
ksk_gadget_base_logunsigned
ksk_gadget_num_levelsunsigned

CGGI ops

cggi.and (heir::cggi::AndOp)

Logical AND of two ciphertexts.

Syntax:

operation ::= `cggi.and` operands attr-dict `:` qualified(type($output))

Traits: AlwaysSpeculatableImplTrait, Commutative, Elementwise, SameOperandsAndResultType, Scalarizable, Tensorizable, Vectorizable

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
lhsciphertext-like
rhsciphertext-like

Results:

ResultDescription
outputciphertext-like

cggi.lut2 (heir::cggi::Lut2Op)

A lookup table on two inputs.

Syntax:

operation ::= `cggi.lut2` operands attr-dict `:` qualified(type($output))

An op representing a lookup table applied to some number n of ciphertexts encrypting boolean input bits.

Over cleartext bits a, b, c, using n = 3 for example, the operation computed by this function can be interpreted as

  truth_table >> {c, b, a}

where {c, b, a} is the unsigned 3-bit integer with bits c, b, a from most significant bit to least-significant bit. The input are combined into a single ciphertext input to the lookup table using products with plaintexts and sums.

Traits: AlwaysSpeculatableImplTrait, Commutative, Elementwise, Scalarizable, Tensorizable, Vectorizable

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, LUTOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
lookup_table::mlir::IntegerAttrAn Attribute containing a integer value

Operands:

OperandDescription
bciphertext-like
aciphertext-like

Results:

ResultDescription
outputciphertext-like

cggi.lut3 (heir::cggi::Lut3Op)

A lookup table on three inputs.

Syntax:

operation ::= `cggi.lut3` operands attr-dict `:` qualified(type($output))

An op representing a lookup table applied to some number n of ciphertexts encrypting boolean input bits.

Over cleartext bits a, b, c, using n = 3 for example, the operation computed by this function can be interpreted as

  truth_table >> {c, b, a}

where {c, b, a} is the unsigned 3-bit integer with bits c, b, a from most significant bit to least-significant bit. The input are combined into a single ciphertext input to the lookup table using products with plaintexts and sums.

Traits: AlwaysSpeculatableImplTrait, Commutative, Elementwise, Scalarizable, Tensorizable, Vectorizable

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, LUTOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
lookup_table::mlir::IntegerAttrAn Attribute containing a integer value

Operands:

OperandDescription
cciphertext-like
bciphertext-like
aciphertext-like

Results:

ResultDescription
outputciphertext-like

cggi.lut_lincomb (heir::cggi::LutLinCombOp)

A variadic-input lookup table with inputs prepared via linear combination.

Syntax:

operation ::= `cggi.lut_lincomb` operands attr-dict `:` type($output)

An op representing a lookup table applied to an arbitrary number of input ciphertexts, which are combined according to a static linear combination attached to the op.

The user must ensure the chosen linear combination does not bleed error bits into the message space according to the underlying ciphertext’s encoding attributes. E.g., a bit_field_encoding with 3 cleartext bits cannot be multiplied by 16.

Example:

#encoding = #lwe.bit_field_encoding<cleartext_start=30, cleartext_bitwidth=3>
#params = #lwe.lwe_params<cmod=7917, dimension=4>
!ciphertext = !lwe.lwe_ciphertext<encoding = #encoding, lwe_params = #params>

%4 = cggi.lut_lincomb %c0, %c1, %c2, %c3 {coefficients = array<i32: 1, 2, 3, 2>, lookup_table = 68 : index} : !ciphertext

Represents applying the lut

  68 >> (1 * c0 + 2 * c1 + 3 * c2 + 2 * c3)

Traits: AlwaysSpeculatableImplTrait, Commutative, Elementwise, SameOperandsAndResultType, Scalarizable, Tensorizable, Vectorizable

Interfaces: ConditionallySpeculatable, LUTOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
coefficients::mlir::DenseI32ArrayAttri32 dense array attribute
lookup_table::mlir::IntegerAttrAn Attribute containing a integer value

Operands:

OperandDescription
inputsvariadic of A type for LWE ciphertexts

Results:

ResultDescription
outputA type for LWE ciphertexts

cggi.multi_lut_lincomb (heir::cggi::MultiLutLinCombOp)

A multi-output version of lut_lincomb with one LUT per output.

Syntax:

operation ::= `cggi.multi_lut_lincomb` operands attr-dict `:` functional-type($inputs, $outputs)

An op representing multiple lookup tables applied to a shared input, which is prepared via a static linear combination. This is equivalent to cggi.lut_lincomb, but where the linear combination is given to multiple lookup tables, each producing a separate output.

This can be achieved by a special implementation of blind rotate in the CGGI scheme. See AutoHoG.

Example:

#encoding = #lwe.bit_field_encoding<cleartext_start=30, cleartext_bitwidth=3>
#params = #lwe.lwe_params<cmod=7917, dimension=4>
!ciphertext = !lwe.lwe_ciphertext<encoding = #encoding, lwe_params = #params>

%4 = cggi.multi_lut_lincomb %c0, %c1, %c2, %c3 {
    coefficients = array<i32: 1, 2, 3, 2>,
    lookup_tables = array<index: 68, 70, 4, 8>
} : (!ciphertext, !ciphertext, !ciphertext, !ciphertext) -> (!ciphertext, !ciphertext, !ciphertext, !ciphertext)

Represents applying the following LUTs. Performance-wise, this is comparable to applying a single LUT to a linear combination.

x = (1 * c0 + 2 * c1 + 3 * c2 + 2 * c3)
return (
  (68 >> x) & 1,
  (70 >> x) & 1,
  (4 >> x) & 1,
  (8 >> x) & 1
)

Traits: AlwaysSpeculatableImplTrait, Commutative, Elementwise, Scalarizable, Tensorizable, Vectorizable

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
coefficients::mlir::DenseI32ArrayAttri32 dense array attribute
lookup_tables::mlir::DenseI32ArrayAttri32 dense array attribute

Operands:

OperandDescription
inputsvariadic of A type for LWE ciphertexts

Results:

ResultDescription
outputsvariadic of A type for LWE ciphertexts

cggi.nand (heir::cggi::NandOp)

Logical NAND of two ciphertexts.

Syntax:

operation ::= `cggi.nand` operands attr-dict `:` qualified(type($output))

Traits: AlwaysSpeculatableImplTrait, Commutative, Elementwise, SameOperandsAndResultType, Scalarizable, Tensorizable, Vectorizable

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
lhsciphertext-like
rhsciphertext-like

Results:

ResultDescription
outputciphertext-like

cggi.nor (heir::cggi::NorOp)

Logical NOR of two ciphertexts.

Syntax:

operation ::= `cggi.nor` operands attr-dict `:` qualified(type($output))

Traits: AlwaysSpeculatableImplTrait, Commutative, Elementwise, SameOperandsAndResultType, Scalarizable, Tensorizable, Vectorizable

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
lhsciphertext-like
rhsciphertext-like

Results:

ResultDescription
outputciphertext-like

cggi.not (heir::cggi::NotOp)

Logical NOT of two ciphertexts

Syntax:

operation ::= `cggi.not` operands attr-dict `:` qualified(type($output))

Traits: AlwaysSpeculatableImplTrait, Elementwise, Involution, SameOperandsAndResultType, Scalarizable, Tensorizable, Vectorizable

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
inputciphertext-like

Results:

ResultDescription
outputciphertext-like

cggi.or (heir::cggi::OrOp)

Logical OR of two ciphertexts.

Syntax:

operation ::= `cggi.or` operands attr-dict `:` qualified(type($output))

Traits: AlwaysSpeculatableImplTrait, Commutative, Elementwise, SameOperandsAndResultType, Scalarizable, Tensorizable, Vectorizable

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
lhsciphertext-like
rhsciphertext-like

Results:

ResultDescription
outputciphertext-like

cggi.xnor (heir::cggi::XNorOp)

Logical XNOR of two ciphertexts.

Syntax:

operation ::= `cggi.xnor` operands attr-dict `:` qualified(type($output))

Traits: AlwaysSpeculatableImplTrait, Commutative, Elementwise, SameOperandsAndResultType, Scalarizable, Tensorizable, Vectorizable

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
lhsciphertext-like
rhsciphertext-like

Results:

ResultDescription
outputciphertext-like

cggi.xor (heir::cggi::XorOp)

Logical XOR of two ciphertexts.

Syntax:

operation ::= `cggi.xor` operands attr-dict `:` qualified(type($output))

Traits: AlwaysSpeculatableImplTrait, Commutative, Elementwise, SameOperandsAndResultType, Scalarizable, Tensorizable, Vectorizable

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
lhsciphertext-like
rhsciphertext-like

Results:

ResultDescription
outputciphertext-like

3.4 - Comb

Types and operations for comb dialect This dialect defines the comb dialect, which is intended to be a generic representation of combinational logic outside of a particular use-case.

Operations

comb.add (heir::comb::AddOp)

Syntax:

operation ::= `comb.add` (`bin` $twoState^)? $inputs attr-dict `:` qualified(type($result))

Traits: AlwaysSpeculatableImplTrait, Commutative, SameOperandsAndResultType, SameTypeOperands

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
twoState::mlir::UnitAttrunit attribute

Operands:

OperandDescription
inputsvariadic of signless integer

Results:

ResultDescription
resultsignless integer

comb.and (heir::comb::AndOp)

Syntax:

operation ::= `comb.and` (`bin` $twoState^)? $inputs attr-dict `:` qualified(type($result))

Traits: AlwaysSpeculatableImplTrait, Commutative, SameOperandsAndResultType, SameTypeOperands

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
twoState::mlir::UnitAttrunit attribute

Operands:

OperandDescription
inputsvariadic of signless integer

Results:

ResultDescription
resultsignless integer

comb.concat (heir::comb::ConcatOp)

Concatenate a variadic list of operands together.

Syntax:

operation ::= `comb.concat` $inputs attr-dict `:` qualified(type($inputs))

See the comb rationale document for details on operand ordering.

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
inputsvariadic of signless integer

Results:

ResultDescription
resultsignless integer

comb.extract (heir::comb::ExtractOp)

Extract a range of bits into a smaller value, lowBit specifies the lowest bit included.

Syntax:

operation ::= `comb.extract` $input `from` $lowBit attr-dict `:` functional-type($input, $result)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
lowBit::mlir::IntegerAttr32-bit signless integer attribute

Operands:

OperandDescription
inputsignless integer

Results:

ResultDescription
resultsignless integer

comb.icmp (heir::comb::ICmpOp)

Compare two integer values

Syntax:

operation ::= `comb.icmp` (`bin` $twoState^)? $predicate $lhs `,` $rhs attr-dict `:` qualified(type($lhs))

This operation compares two integers using a predicate. If the predicate is true, returns 1, otherwise returns 0. This operation always returns a one bit wide result.

    %r = comb.icmp eq %a, %b : i4

Traits: AlwaysSpeculatableImplTrait, SameTypeOperands

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
predicate::mlir::heir::comb::ICmpPredicateAttrhw.icmp comparison predicate
twoState::mlir::UnitAttrunit attribute

Operands:

OperandDescription
lhssignless integer
rhssignless integer

Results:

ResultDescription
result1-bit signless integer

comb.inv (heir::comb::InvOp)

Syntax:

operation ::= `comb.inv` (`bin` $twoState^)? $input attr-dict `:` qualified(type($input))

Traits: AlwaysSpeculatableImplTrait, SameOperandsAndResultType

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
twoState::mlir::UnitAttrunit attribute

Operands:

OperandDescription
inputsignless integer

Results:

ResultDescription
resultsignless integer

comb.mul (heir::comb::MulOp)

Syntax:

operation ::= `comb.mul` (`bin` $twoState^)? $inputs attr-dict `:` qualified(type($result))

Traits: AlwaysSpeculatableImplTrait, Commutative, SameOperandsAndResultType, SameTypeOperands

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
twoState::mlir::UnitAttrunit attribute

Operands:

OperandDescription
inputsvariadic of signless integer

Results:

ResultDescription
resultsignless integer

comb.mux (heir::comb::MuxOp)

Return one or the other operand depending on a selector bit

Syntax:

operation ::= `comb.mux` (`bin` $twoState^)? $cond `,` $trueValue `,` $falseValue  attr-dict `:` qualified(type($result))
  %0 = mux %pred, %tvalue, %fvalue : i4

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
twoState::mlir::UnitAttrunit attribute

Operands:

OperandDescription
cond1-bit signless integer
trueValueany type
falseValueany type

Results:

ResultDescription
resultany type

comb.nand (heir::comb::NandOp)

Syntax:

operation ::= `comb.nand` (`bin` $twoState^)? $inputs attr-dict `:` qualified(type($result))

Traits: AlwaysSpeculatableImplTrait, SameOperandsAndResultType, SameTypeOperands

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
twoState::mlir::UnitAttrunit attribute

Operands:

OperandDescription
inputsvariadic of signless integer

Results:

ResultDescription
resultsignless integer

comb.nor (heir::comb::NorOp)

Syntax:

operation ::= `comb.nor` (`bin` $twoState^)? $inputs attr-dict `:` qualified(type($result))

Traits: AlwaysSpeculatableImplTrait, SameOperandsAndResultType, SameTypeOperands

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
twoState::mlir::UnitAttrunit attribute

Operands:

OperandDescription
inputsvariadic of signless integer

Results:

ResultDescription
resultsignless integer

comb.or (heir::comb::OrOp)

Syntax:

operation ::= `comb.or` (`bin` $twoState^)? $inputs attr-dict `:` qualified(type($result))

Traits: AlwaysSpeculatableImplTrait, Commutative, SameOperandsAndResultType, SameTypeOperands

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
twoState::mlir::UnitAttrunit attribute

Operands:

OperandDescription
inputsvariadic of signless integer

Results:

ResultDescription
resultsignless integer

comb.parity (heir::comb::ParityOp)

Syntax:

operation ::= `comb.parity` (`bin` $twoState^)? $input attr-dict `:` qualified(type($input))

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
twoState::mlir::UnitAttrunit attribute

Operands:

OperandDescription
inputsignless integer

Results:

ResultDescription
result1-bit signless integer

comb.replicate (heir::comb::ReplicateOp)

Concatenate the operand a constant number of times

Syntax:

operation ::= `comb.replicate` $input attr-dict `:` functional-type($input, $result)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
inputsignless integer

Results:

ResultDescription
resultsignless integer

comb.truth_table (heir::comb::TruthTableOp)

Return a true/false based on a lookup table

Syntax:

operation ::= `comb.truth_table` $inputs `->` $lookupTable attr-dict
  %a = ... : i1
  %b = ... : i1
  %0 = comb.truth_table %a, %b -> 6 : ui4

This operation assumes that the lookup table is described as an integer of 2^n bits to fully specify the table. Inputs are sorted MSB -> LSB from left to right and the offset into lookupTable is computed from them. The integer containing the truth table value’s LSB is the output for the input “all false”, and the MSB is the output for the input “all true”.

No difference from array_get into an array of constants except for xprop behavior. If one of the inputs is unknown, but said input doesn’t make a difference in the output (based on the lookup table) the result should not be ‘x’ – it should be the well-known result.

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, LUTOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
lookupTable::mlir::IntegerAttrAn Attribute containing a integer value

Operands:

OperandDescription
inputsvariadic of 1-bit signless integer

Results:

ResultDescription
result1-bit signless integer

comb.xnor (heir::comb::XNorOp)

Syntax:

operation ::= `comb.xnor` (`bin` $twoState^)? $inputs attr-dict `:` qualified(type($result))

Traits: AlwaysSpeculatableImplTrait, SameOperandsAndResultType, SameTypeOperands

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
twoState::mlir::UnitAttrunit attribute

Operands:

OperandDescription
inputsvariadic of signless integer

Results:

ResultDescription
resultsignless integer

comb.xor (heir::comb::XorOp)

Syntax:

operation ::= `comb.xor` (`bin` $twoState^)? $inputs attr-dict `:` qualified(type($result))

Traits: AlwaysSpeculatableImplTrait, Commutative, SameOperandsAndResultType, SameTypeOperands

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
twoState::mlir::UnitAttrunit attribute

Operands:

OperandDescription
inputsvariadic of signless integer

Results:

ResultDescription
resultsignless integer

Enums

ICmpPredicate

hw.icmp comparison predicate

Cases:

SymbolValueString
eq0eq
ne1ne
slt2slt
sle3sle
sgt4sgt
sge5sge
ult6ult
ule7ule
ugt8ugt
uge9uge
ceq10ceq
cne11cne
weq12weq
wne13wne

Comb types

Comb ops

comb.add (heir::comb::AddOp)

Syntax:

operation ::= `comb.add` (`bin` $twoState^)? $inputs attr-dict `:` qualified(type($result))

Traits: AlwaysSpeculatableImplTrait, Commutative, SameOperandsAndResultType, SameTypeOperands

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
twoState::mlir::UnitAttrunit attribute

Operands:

OperandDescription
inputsvariadic of signless integer

Results:

ResultDescription
resultsignless integer

comb.and (heir::comb::AndOp)

Syntax:

operation ::= `comb.and` (`bin` $twoState^)? $inputs attr-dict `:` qualified(type($result))

Traits: AlwaysSpeculatableImplTrait, Commutative, SameOperandsAndResultType, SameTypeOperands

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
twoState::mlir::UnitAttrunit attribute

Operands:

OperandDescription
inputsvariadic of signless integer

Results:

ResultDescription
resultsignless integer

comb.concat (heir::comb::ConcatOp)

Concatenate a variadic list of operands together.

Syntax:

operation ::= `comb.concat` $inputs attr-dict `:` qualified(type($inputs))

See the comb rationale document for details on operand ordering.

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
inputsvariadic of signless integer

Results:

ResultDescription
resultsignless integer

comb.extract (heir::comb::ExtractOp)

Extract a range of bits into a smaller value, lowBit specifies the lowest bit included.

Syntax:

operation ::= `comb.extract` $input `from` $lowBit attr-dict `:` functional-type($input, $result)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
lowBit::mlir::IntegerAttr32-bit signless integer attribute

Operands:

OperandDescription
inputsignless integer

Results:

ResultDescription
resultsignless integer

comb.icmp (heir::comb::ICmpOp)

Compare two integer values

Syntax:

operation ::= `comb.icmp` (`bin` $twoState^)? $predicate $lhs `,` $rhs attr-dict `:` qualified(type($lhs))

This operation compares two integers using a predicate. If the predicate is true, returns 1, otherwise returns 0. This operation always returns a one bit wide result.

    %r = comb.icmp eq %a, %b : i4

Traits: AlwaysSpeculatableImplTrait, SameTypeOperands

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
predicate::mlir::heir::comb::ICmpPredicateAttrhw.icmp comparison predicate
twoState::mlir::UnitAttrunit attribute

Operands:

OperandDescription
lhssignless integer
rhssignless integer

Results:

ResultDescription
result1-bit signless integer

comb.inv (heir::comb::InvOp)

Syntax:

operation ::= `comb.inv` (`bin` $twoState^)? $input attr-dict `:` qualified(type($input))

Traits: AlwaysSpeculatableImplTrait, SameOperandsAndResultType

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
twoState::mlir::UnitAttrunit attribute

Operands:

OperandDescription
inputsignless integer

Results:

ResultDescription
resultsignless integer

comb.mul (heir::comb::MulOp)

Syntax:

operation ::= `comb.mul` (`bin` $twoState^)? $inputs attr-dict `:` qualified(type($result))

Traits: AlwaysSpeculatableImplTrait, Commutative, SameOperandsAndResultType, SameTypeOperands

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
twoState::mlir::UnitAttrunit attribute

Operands:

OperandDescription
inputsvariadic of signless integer

Results:

ResultDescription
resultsignless integer

comb.mux (heir::comb::MuxOp)

Return one or the other operand depending on a selector bit

Syntax:

operation ::= `comb.mux` (`bin` $twoState^)? $cond `,` $trueValue `,` $falseValue  attr-dict `:` qualified(type($result))
  %0 = mux %pred, %tvalue, %fvalue : i4

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
twoState::mlir::UnitAttrunit attribute

Operands:

OperandDescription
cond1-bit signless integer
trueValueany type
falseValueany type

Results:

ResultDescription
resultany type

comb.nand (heir::comb::NandOp)

Syntax:

operation ::= `comb.nand` (`bin` $twoState^)? $inputs attr-dict `:` qualified(type($result))

Traits: AlwaysSpeculatableImplTrait, SameOperandsAndResultType, SameTypeOperands

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
twoState::mlir::UnitAttrunit attribute

Operands:

OperandDescription
inputsvariadic of signless integer

Results:

ResultDescription
resultsignless integer

comb.nor (heir::comb::NorOp)

Syntax:

operation ::= `comb.nor` (`bin` $twoState^)? $inputs attr-dict `:` qualified(type($result))

Traits: AlwaysSpeculatableImplTrait, SameOperandsAndResultType, SameTypeOperands

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
twoState::mlir::UnitAttrunit attribute

Operands:

OperandDescription
inputsvariadic of signless integer

Results:

ResultDescription
resultsignless integer

comb.or (heir::comb::OrOp)

Syntax:

operation ::= `comb.or` (`bin` $twoState^)? $inputs attr-dict `:` qualified(type($result))

Traits: AlwaysSpeculatableImplTrait, Commutative, SameOperandsAndResultType, SameTypeOperands

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
twoState::mlir::UnitAttrunit attribute

Operands:

OperandDescription
inputsvariadic of signless integer

Results:

ResultDescription
resultsignless integer

comb.parity (heir::comb::ParityOp)

Syntax:

operation ::= `comb.parity` (`bin` $twoState^)? $input attr-dict `:` qualified(type($input))

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
twoState::mlir::UnitAttrunit attribute

Operands:

OperandDescription
inputsignless integer

Results:

ResultDescription
result1-bit signless integer

comb.replicate (heir::comb::ReplicateOp)

Concatenate the operand a constant number of times

Syntax:

operation ::= `comb.replicate` $input attr-dict `:` functional-type($input, $result)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
inputsignless integer

Results:

ResultDescription
resultsignless integer

comb.truth_table (heir::comb::TruthTableOp)

Return a true/false based on a lookup table

Syntax:

operation ::= `comb.truth_table` $inputs `->` $lookupTable attr-dict
  %a = ... : i1
  %b = ... : i1
  %0 = comb.truth_table %a, %b -> 6 : ui4

This operation assumes that the lookup table is described as an integer of 2^n bits to fully specify the table. Inputs are sorted MSB -> LSB from left to right and the offset into lookupTable is computed from them. The integer containing the truth table value’s LSB is the output for the input “all false”, and the MSB is the output for the input “all true”.

No difference from array_get into an array of constants except for xprop behavior. If one of the inputs is unknown, but said input doesn’t make a difference in the output (based on the lookup table) the result should not be ‘x’ – it should be the well-known result.

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, LUTOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
lookupTable::mlir::IntegerAttrAn Attribute containing a integer value

Operands:

OperandDescription
inputsvariadic of 1-bit signless integer

Results:

ResultDescription
result1-bit signless integer

comb.xnor (heir::comb::XNorOp)

Syntax:

operation ::= `comb.xnor` (`bin` $twoState^)? $inputs attr-dict `:` qualified(type($result))

Traits: AlwaysSpeculatableImplTrait, SameOperandsAndResultType, SameTypeOperands

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
twoState::mlir::UnitAttrunit attribute

Operands:

OperandDescription
inputsvariadic of signless integer

Results:

ResultDescription
resultsignless integer

comb.xor (heir::comb::XorOp)

Syntax:

operation ::= `comb.xor` (`bin` $twoState^)? $inputs attr-dict `:` qualified(type($result))

Traits: AlwaysSpeculatableImplTrait, Commutative, SameOperandsAndResultType, SameTypeOperands

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
twoState::mlir::UnitAttrunit attribute

Operands:

OperandDescription
inputsvariadic of signless integer

Results:

ResultDescription
resultsignless integer

3.5 - Jaxite

The jaxite dialect is an exit dialect for generating py code against the jaxite library API, using the jaxite parameters and encoding scheme.

See https://github.com/google/jaxite

Jaxite types

ParamsType

The jaxite security params required to perform homomorphic operations.

Syntax: !jaxite.params

ServerKeySetType

The jaxite server key set required to perform homomorphic operations.

Syntax: !jaxite.server_key_set

Jaxite ops

jaxite.constant (heir::jaxite::ConstantOp)

Syntax:

operation ::= `jaxite.constant` operands attr-dict `:` functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
value1-bit signless integer
paramsThe jaxite security params required to perform homomorphic operations.

Results:

ResultDescription
outputA type for LWE ciphertexts

jaxite.lut3 (heir::jaxite::Lut3Op)

Syntax:

operation ::= `jaxite.lut3` operands attr-dict `:` functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
aA type for LWE ciphertexts
bA type for LWE ciphertexts
cA type for LWE ciphertexts
truth_table8-bit signless integer
serverKeySetThe jaxite server key set required to perform homomorphic operations.
paramsThe jaxite security params required to perform homomorphic operations.

Results:

ResultDescription
outputA type for LWE ciphertexts

3.6 - LWE

LWE attributes

BitFieldEncodingAttr

An attribute describing encoded LWE plaintexts using bit fields.

Syntax:

#lwe.bit_field_encoding<
  unsigned,   # cleartext_start
  unsigned   # cleartext_bitwidth
>

A bit field encoding of an integer describes which contiguous region of bits a small integer occupies within a larger integer.

The data describing the encoding consists of the starting bit positions of the cleartext bit field and its width, where the LSB is bit 0 and the MSB is bit bit_width-1. So the above example would have starting bit 30 and width 3. The bits not specified for the message have semantics defined by the scheme or lowering.

Note that this encoding does not specify the underlying bit width of the plaintext space. This is left for lowerings to decide.

The presence of this attribute as the encoding attribute of a tensor indicates that the tensor is an LWE ciphertext.

Example (CGGI):

#encoding = #lwe.bit_field_encoding<cleartext_start=30, cleartext_bitwidth=3>
!plaintext = !lwe.lwe_plaintext<encoding = #encoding>

%0 = arith.constant 4 : i3
%1 = lwe.encode %0 { encoding = #encoding }: i3 to !plaintext

The above represents an LWE plaintext encoding the 3-bit cleartext 4 as an LWE ciphertext in a 32-bit integer, with a single bit of padding at the MSB. This corresponds to the following, where 0 denotes a 0 bit, b denotes a bit of the cleartext, n denotes a bit reserved for noise, and | is a visual aid to show where the bit fields begin and end.

   0|bbb|nn...n
MSB^          ^LSB

Example (BGV):

Note: BGV uses the RLWE encodings, but they have the same bit-field encoding attributes as here. So this example serves mainly to show how this attribute can be used to specify storing bits in the LSB of a plaintext.

#encoding = #lwe.bit_field_encoding<cleartext_start=4, cleartext_bitwidth=4>
!plaintext = !lwe.lwe_plaintext<encoding = #encoding>

%0 = arith.constant 9 : i4
%1 = lwe.encode %0 { encoding = #encoding }: i4 to !plaintext

The above represents an LWE plaintext encoding a 4-bit cleartext as an LWE ciphertext in the least-significant bits of a larger integer. This corresponds to the following.

   nn...n|bbbb
MSB^         ^LSB

Parameters:

ParameterC++ typeDescription
cleartext_startunsigned
cleartext_bitwidthunsigned

LWEParamsAttr

Syntax:

#lwe.lwe_params<
  IntegerAttr,   # cmod
  unsigned   # dimension
>

Parameters:

ParameterC++ typeDescription
cmodIntegerAttr
dimensionunsigned

RLWEParamsAttr

Syntax:

#lwe.rlwe_params<
  unsigned,   # dimension
  ::mlir::polynomial::RingAttr   # ring
>

An attribute describing classical RLWE parameters:

  • dimension: the number of polynomials used in an RLWE sample, analogous to LWEParams.dimension.
  • ring: the polynomial ring to use.

Parameters:

ParameterC++ typeDescription
dimensionunsigned
ring::mlir::polynomial::RingAttr

UnspecifiedBitFieldEncodingAttr

An attribute describing unspecified bit field encodings.

Syntax:

#lwe.unspecified_bit_field_encoding<
  unsigned   # cleartext_bitwidth
>

See LWE_BitFieldEncoding for a description of bit field encodings.

This attribute describes an unspecified bit field encoding; this is where the starting bit position of the cleartext bit field is unspecified, but its width is fixed. A noise growth analysis should be performed to determine the optimal amount of bits needed for noise and padding to specify the bit field encodings starting bit position.

Example:

#lwe_encoding = #lwe.unspecified_bit_field_encoding<cleartext_bitwidth=3>
%lwe_ciphertext = arith.constant <[1,2,3,4]> : tensor<4xi32, #lwe_encoding>

Parameters:

ParameterC++ typeDescription
cleartext_bitwidthunsigned

InverseCanonicalEmbeddingEncodingAttr

An attribute describing encoded RLWE plaintexts via the rounded inverse canonical embedding.

Syntax:

#lwe.inverse_canonical_embedding_encoding<
  unsigned,   # cleartext_start
  unsigned   # cleartext_bitwidth
>

Let $n$ be the degree of the polynomials in the plaintext space. An “inverse canonical embedding encoding” of a list of real or complex values $v_1, \dots, v_{n/2}$ is (almost) the inverse of the following decoding map.

Define a map $\tau_N$ that maps a polynomial $p \in \mathbb{Z}[x] / (x^N + 1) \to \mathbb{C}^{N/2}$ by evaluating it at the following $N/2$ points, where $\omega = e^{2 \pi i / 2N}$ is the primitive $2N$th root of unity:

[ \omega, \omega^3, \omega^5, \dots, \omega^{N-1} ]

Then the complete decoding operation is $\textup{Decode}(p) = (1/\Delta)\tau_N(p)$, where $\Delta$ is a scaling parameter and $\tau_N$ is the truncated canonical embedding above. The encoding operation is the inverse of the decoding operation, with some caveats explained below.

The map $\tau_N$ is derived from the so-called canonical embedding $\tau$, though in the standard canonical embedding, we evaluate at all odd powers of the root of unity, $\omega, \omega^3, \dots, \omega^{2N-1}$. For polynomials in the slightly larger space $\mathbb{R}[x] / (x^N + 1)$, the image of the canonical embedding is the subspace $H \subset \mathbb{C}^N$ defined by tuples $(z_1, \dots, z_N)$ such that $\overline{z_i} = \overline{z_{N-i+1}}$. Note that this property holds because polynomial evaluation commutes with complex conjugates, and the second half of the roots of unity evaluate are complex conjugates of the first half. The converse, that any such tuple with complex conjugate symmetry has an inverse under $\tau$ with all real coefficients, makes $\tau$ is a bijection onto $H$. $\tau$ and its inverse are explicitly computable as discrete Fourier Transforms.

Because of the symmetry in canonical embedding for real polynomials, inputs to this encoding can be represented as a list of $N/2$ complex points, with the extra symmetric structure left implicit. $\tau_N$ and its inverse can also be explicitly computed without need to expand the vectors to length $N$.

The rounding step is required to invert the decoding because, while cleartexts must be (implicitly) in the subspace $H$, they need not be the output of $\tau_N$ for an integer polynomial. The rounding step ensures we can use integer polynomial plaintexts for the FHE operations. There are multiple rounding mechanisms, and this attribute does not specify which is used, because in theory two ciphertexts that have used different roundings are still compatible, though they may have different noise growth patterns.

The scaling parameter $\Delta$ is specified by the cleartext_start and cleartext_bitwidth parameters, which are applied coefficient-wise using the same semantics as the bit_field_encoding.

This attribute can be used in multiple ways:

  • On a poly.poly, it asserts that the polynomial has been transformed from a coefficient list using the canonical embedding.
  • On a tensor of poly.poly, it asserts that the tensor is an RLWE ciphertext for some RLWE scheme that supports the approximate embedding encoding.

A typical flow for the CKKS scheme using this encoding would be to apply an inverse FFT operation to invert the canonical embedding to be a polynomial with real coefficients, then encrypt scale the resulting polynomial’s coefficients according to the scaling parameters, then round to get integer coefficients.

Example:

#generator = #poly.polynomial<1 + x**1024>
#ring = #poly.ring<cmod=65536, ideal=#generator>
#lwe_encoding = #lwe.polynomial_evaluation_encoding<cleartext_start=30, cleartext_bitwidth=3>

%evals = arith.constant <[1, 2, 4, 5]> : tensor<4xi16>
%poly1 = poly.intt %evals : tensor<4xi16, #ring> -> !poly.poly<#ring, #eval_encoding>
%poly2 = poly.intt %evals : tensor<4xi16, #ring> -> !poly.poly<#ring, #eval_encoding>
%rlwe_ciphertext = tensor.from_elements %poly1, %poly2 : tensor<2x!poly.poly<#ring, #eval_encoding>>

See bit_field_encoding for the definition of the cleartext_start and cleartext_bitwidth fields.

Parameters:

ParameterC++ typeDescription
cleartext_startunsigned
cleartext_bitwidthunsigned

PolynomialCoefficientEncodingAttr

An attribute describing encoded RLWE plaintexts via coefficients.

Syntax:

#lwe.polynomial_coefficient_encoding<
  unsigned,   # cleartext_start
  unsigned   # cleartext_bitwidth
>

A coefficient encoding of a list of integers asserts that the coefficients of the polynomials contain the cleartexts, with the same semantics as bit_field_encoding for per-coefficient encodings.

The presence of this attribute as the encoding attribute of a tensor of poly.poly indicates that the tensor is an RLWE ciphertext for some RLWE scheme that supports the coefficient encoding.

See bit_field_encoding for the definition of the cleartext_start and cleartext_bitwidth fields.

Example:

#generator = #poly.polynomial<1 + x**1024>
#ring = #poly.ring<cmod=65536, ideal=#generator>
#coeff_encoding = #lwe.polynomial_coefficient_encoding<cleartext_start=15, cleartext_bitwidth=4>

%poly1 = poly.from_tensor %coeffs1 : tensor<10xi16> -> !poly.poly<#ring>
%poly2 = poly.from_tensor %coeffs2 : tensor<10xi16> -> !poly.poly<#ring>
%rlwe_ciphertext = tensor.from_elements %poly1, %poly2 : tensor<2x!poly.poly<#ring>, #coeff_encoding>

Parameters:

ParameterC++ typeDescription
cleartext_startunsigned
cleartext_bitwidthunsigned

PolynomialEvaluationEncodingAttr

An attribute describing encoded RLWE plaintexts via evaluations at fixed points.

Syntax:

#lwe.polynomial_evaluation_encoding<
  unsigned,   # cleartext_start
  unsigned   # cleartext_bitwidth
>

A “evaluation encoding” of a list of integers $(v_1, \dots, v_n)$ asserts that $f(x_1 ) = v_1, \dots, f(x_n) = v_n$ for some implicit, but fixed and distinct choice of inputs $x_i$. The encoded values are also scaled by a scale factor, having the same semantics as bit_field_encoding, but applied entry-wise (to either the coefficient or evaluation representation).

This attribute can be used in multiple ways:

  • On a poly.poly, it asserts that the polynomial has been transformed from an evaluation tensor.
  • On a tensor of poly.poly, it asserts that the tensor is an RLWE ciphertext for some RLWE scheme that supports the evaluation encoding.

A typical workflow for the BFV/BGV schemes using this encoding would be to apply a INTT operation to the input list of cleartexts to convert from evaluation form to coefficient form, then encrypt the resulting polynomial in coefficient form, then apply NTT back to the evaluation form for faster multiplication of ciphertexts.

The points chosen are fixed to be the powers of a primitive root of unity of the coefficient ring of the plaintext space, which allows one to use NTT/INTT to tansform quickly between the coefficient and evaluation forms.

Example:

#generator = #poly.polynomial<1 + x**1024>
// note that the cmod should be chosen so as to ensure a primitive root of
// unity exists in the multiplicative group (Z / cmod Z)^*
#ring = #poly.ring<cmod=65536, ideal=#generator>
#lwe_encoding = #lwe.polynomial_evaluation_encoding<cleartext_start=30, cleartext_bitwidth=3>

%evals = arith.constant <[1, 2, 4, 5]> : tensor<4xi16>
%poly1 = poly.intt %evals : tensor<4xi16, #ring> -> !poly.poly<#ring, #eval_encoding>
%poly2 = poly.intt %evals : tensor<4xi16, #ring> -> !poly.poly<#ring, #eval_encoding>
%rlwe_ciphertext = tensor.from_elements %poly1, %poly2 : tensor<2x!poly.poly<#ring, #eval_encoding>>

See bit_field_encoding for the definition of the cleartext_start and cleartext_bitwidth fields.

Parameters:

ParameterC++ typeDescription
cleartext_startunsigned
cleartext_bitwidthunsigned

LWE types

LWECiphertextType

A type for LWE ciphertexts

Syntax:

!lwe.lwe_ciphertext<
  ::mlir::Attribute,   # encoding
  LWEParamsAttr   # lwe_params
>

A type for LWE ciphertexts.

This type keeps track of the plaintext integer encoding for the LWE Ciphertext to ensure proper decoding after decryption. It also keeps track of the ring where the LWE ciphertext is defined, which provides information on the ciphertext shape and the ring operations used in LWE operations.

Parameters:

ParameterC++ typeDescription
encoding::mlir::Attribute
lwe_paramsLWEParamsAttr

LWEPlaintextType

A type for LWE plaintexts

Syntax:

!lwe.lwe_plaintext<
  ::mlir::Attribute   # encoding
>

A type for LWE plaintexts.

This type keeps track of the plaintext integer encoding for the LWE plaintext before it is encrypted.

Parameters:

ParameterC++ typeDescription
encoding::mlir::Attribute

RLWECiphertextType

A type for RLWE ciphertexts

Syntax:

!lwe.rlwe_ciphertext<
  ::mlir::Attribute,   # encoding
  RLWEParamsAttr,   # rlwe_params
  Type   # underlying_type
>

Parameters:

ParameterC++ typeDescription
encoding::mlir::Attribute
rlwe_paramsRLWEParamsAttr
underlying_typeType

RLWEPlaintextType

A type for RLWE plaintexts

Syntax:

!lwe.rlwe_plaintext<
  ::mlir::Attribute,   # encoding
  ::mlir::polynomial::RingAttr,   # ring
  Type   # underlying_type
>

Parameters:

ParameterC++ typeDescription
encoding::mlir::Attribute
ring::mlir::polynomial::RingAttr
underlying_typeType

RLWEPublicKeyType

A public key for RLWE

Syntax:

!lwe.rlwe_public_key<
  RLWEParamsAttr   # rlwe_params
>

Parameters:

ParameterC++ typeDescription
rlwe_paramsRLWEParamsAttr

RLWESecretKeyType

A secret key for RLWE

Syntax:

!lwe.rlwe_secret_key<
  RLWEParamsAttr   # rlwe_params
>

Parameters:

ParameterC++ typeDescription
rlwe_paramsRLWEParamsAttr

LWE ops

lwe.add (heir::lwe::AddOp)

Add two LWE ciphertexts

Syntax:

operation ::= `lwe.add` operands attr-dict `:` type($output)

Traits: AlwaysSpeculatableImplTrait, Commutative, SameOperandsAndResultType

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
lhsciphertext-like
rhsciphertext-like

Results:

ResultDescription
outputciphertext-like

lwe.encode (heir::lwe::EncodeOp)

Encode an integer to yield an LWE plaintext

Syntax:

operation ::= `lwe.encode` $plaintext attr-dict `:` qualified(type($plaintext)) `to` qualified(type($output))

Encode an integer to yield an LWE plaintext.

This op uses a an encoding attribute to encode the bits of the integer into an LWE plaintext value that can then be encrypted.

Examples:

%Y = lwe.encode %value {encoding = #enc}: i1 to !lwe.lwe_plaintext<encoding = #enc>

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
encoding::mlir::AttributeAn attribute describing encoded LWE plaintexts using bit fields. or An attribute describing unspecified bit field encodings.

Operands:

OperandDescription
plaintextsignless-integer-like

Results:

ResultDescription
outputA type for LWE plaintexts

lwe.mul_scalar (heir::lwe::MulScalarOp)

Multiply an LWE ciphertext by a scalar

Syntax:

operation ::= `lwe.mul_scalar` operands attr-dict `:`  functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait, Commutative

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
ciphertextciphertext-like
scalarinteger

Results:

ResultDescription
outputciphertext-like

lwe.rlwe_decode (heir::lwe::RLWEDecodeOp)

Decode an RLWE plaintext to an underlying type

Syntax:

operation ::= `lwe.rlwe_decode` $input attr-dict `:` qualified(type($input)) `->` qualified(type($output))

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
encoding::mlir::AttributeAn attribute describing encoded RLWE plaintexts via coefficients. or An attribute describing encoded RLWE plaintexts via evaluations at fixed points. or An attribute describing encoded RLWE plaintexts via the rounded inverse canonical embedding.
ring::mlir::polynomial::RingAttran attribute specifying a polynomial ring

Operands:

OperandDescription
inputA type for RLWE plaintexts

Results:

ResultDescription
outputsignless-integer-like

lwe.rlwe_decrypt (heir::lwe::RLWEDecryptOp)

Decrypt an RLWE ciphertext to a RLWE plaintext

Syntax:

operation ::= `lwe.rlwe_decrypt` operands attr-dict `:`  functional-type(operands, results)

Decrypt an RLWE ciphertext to yield a RLWE plaintext

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
inputA type for RLWE ciphertexts
secret_keyA secret key for RLWE

Results:

ResultDescription
outputA type for RLWE plaintexts

lwe.rlwe_encode (heir::lwe::RLWEEncodeOp)

Encode an integer to yield an RLWE plaintext

Syntax:

operation ::= `lwe.rlwe_encode` $input attr-dict `:` qualified(type($input)) `->` qualified(type($output))

Encode an integer to yield an RLWE plaintext.

This op uses a an encoding attribute to encode the bits of the integer into an RLWE plaintext value that can then be encrypted.

Examples:

%Y = lwe.rlwe_encode %value {encoding = #enc, ring = #ring}: i1 to !lwe.rlwe_plaintext<encoding = #enc, ring = #ring>

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
encoding::mlir::AttributeAn attribute describing encoded RLWE plaintexts via coefficients. or An attribute describing encoded RLWE plaintexts via evaluations at fixed points. or An attribute describing encoded RLWE plaintexts via the rounded inverse canonical embedding.
ring::mlir::polynomial::RingAttran attribute specifying a polynomial ring

Operands:

OperandDescription
inputsignless-integer-like

Results:

ResultDescription
outputA type for RLWE plaintexts

lwe.rlwe_encrypt (heir::lwe::RLWEEncryptOp)

Encrypt an RLWE plaintext to a RLWE ciphertext

Syntax:

operation ::= `lwe.rlwe_encrypt` operands attr-dict `:`  functional-type(operands, results)

Encrypt an RLWE plaintext to yield a RLWE ciphertext.

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
inputA type for RLWE plaintexts
keyA secret key for RLWE or A public key for RLWE

Results:

ResultDescription
outputA type for RLWE ciphertexts

lwe.trivial_encrypt (heir::lwe::TrivialEncryptOp)

Create a trivial encryption of a plaintext.

Syntax:

operation ::= `lwe.trivial_encrypt` operands attr-dict `:`  qualified(type(operands)) `to` qualified(type(results))

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
params::mlir::heir::lwe::LWEParamsAttr

Operands:

OperandDescription
inputA type for LWE plaintexts

Results:

ResultDescription
outputA type for LWE ciphertexts

lwe.reinterpret_underlying_type (heir::lwe::ReinterpretUnderlyingTypeOp)

A placeholder cast from one ciphertext type to another

Syntax:

operation ::= `lwe.reinterpret_underlying_type` $input attr-dict `:` qualified(type($input)) `to` qualified(type($output))

The cast op is thus used to translate underlying_type between ciphertexts in particular situations , such as when lowering to an API that does not keep track of types for you.

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
inputA type for RLWE ciphertexts

Results:

ResultDescription
outputA type for RLWE ciphertexts

3.7 - Openfhe

The openfhe dialect is an exit dialect for generating c++ code against the OpenFHE library API.

See https://github.com/openfheorg/openfhe-development

Openfhe types

CCParamsType

The CCParams required to create CryptoContext.

Syntax: !openfhe.cc_params

CryptoContextType

The CryptoContext required to perform homomorphic operations in OpenFHE.

Syntax: !openfhe.crypto_context

EvalKeyType

The evaluation key required to keyswitch/relinearize/rotate/automorphism operation in OpenFHE.

Syntax: !openfhe.eval_key

PrivateKeyType

The private key required to decrypt a ciphertext in OpenFHE.

Syntax: !openfhe.private_key

PublicKeyType

The public key required to encrypt plaintext in OpenFHE.

Syntax: !openfhe.public_key

Openfhe ops

openfhe.add (heir::openfhe::AddOp)

OpenFHE add operation of two ciphertexts.

Syntax:

operation ::= `openfhe.add` operands attr-dict `:` functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
cryptoContextThe CryptoContext required to perform homomorphic operations in OpenFHE.
lhsA type for RLWE ciphertexts
rhsA type for RLWE ciphertexts

Results:

ResultDescription
outputA type for RLWE ciphertexts

openfhe.automorph (heir::openfhe::AutomorphOp)

Syntax:

operation ::= `openfhe.automorph` operands attr-dict `:` functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
cryptoContextThe CryptoContext required to perform homomorphic operations in OpenFHE.
ciphertextA type for RLWE ciphertexts
evalKeyThe evaluation key required to keyswitch/relinearize/rotate/automorphism operation in OpenFHE.

Results:

ResultDescription
outputA type for RLWE ciphertexts

openfhe.decrypt (heir::openfhe::DecryptOp)

Syntax:

operation ::= `openfhe.decrypt` operands attr-dict `:` functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
cryptoContextThe CryptoContext required to perform homomorphic operations in OpenFHE.
ciphertextA type for RLWE ciphertexts
privateKeyThe private key required to decrypt a ciphertext in OpenFHE.

Results:

ResultDescription
plaintextA type for RLWE plaintexts

openfhe.encrypt (heir::openfhe::EncryptOp)

Syntax:

operation ::= `openfhe.encrypt` operands attr-dict `:` functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
cryptoContextThe CryptoContext required to perform homomorphic operations in OpenFHE.
plaintextA type for RLWE plaintexts
publicKeyThe public key required to encrypt plaintext in OpenFHE.

Results:

ResultDescription
ciphertextA type for RLWE ciphertexts

openfhe.gen_context (heir::openfhe::GenContextOp)

Syntax:

operation ::= `openfhe.gen_context` operands attr-dict `:` functional-type(operands, results)

Interfaces: InferTypeOpInterface

Operands:

OperandDescription
paramsThe CCParams required to create CryptoContext.

Results:

ResultDescription
contextThe CryptoContext required to perform homomorphic operations in OpenFHE.

openfhe.gen_mulkey (heir::openfhe::GenMulKeyOp)

Syntax:

operation ::= `openfhe.gen_mulkey` operands attr-dict `:` functional-type(operands, results)

Operands:

OperandDescription
cryptoContextThe CryptoContext required to perform homomorphic operations in OpenFHE.
privateKeyThe private key required to decrypt a ciphertext in OpenFHE.

openfhe.gen_params (heir::openfhe::GenParamsOp)

Syntax:

operation ::= `openfhe.gen_params` operands attr-dict `:` functional-type(operands, results)

Interfaces: InferTypeOpInterface

Attributes:

AttributeMLIR TypeDescription
mulDepth::mlir::IntegerAttr64-bit signless integer attribute
plainMod::mlir::IntegerAttr64-bit signless integer attribute

Results:

ResultDescription
paramsThe CCParams required to create CryptoContext.

openfhe.gen_rotkey (heir::openfhe::GenRotKeyOp)

Syntax:

operation ::= `openfhe.gen_rotkey` operands attr-dict `:` functional-type(operands, results)

Attributes:

AttributeMLIR TypeDescription
indices::mlir::DenseI64ArrayAttri64 dense array attribute

Operands:

OperandDescription
cryptoContextThe CryptoContext required to perform homomorphic operations in OpenFHE.
privateKeyThe private key required to decrypt a ciphertext in OpenFHE.

openfhe.key_switch (heir::openfhe::KeySwitchOp)

Syntax:

operation ::= `openfhe.key_switch` operands attr-dict `:` functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
cryptoContextThe CryptoContext required to perform homomorphic operations in OpenFHE.
ciphertextA type for RLWE ciphertexts
evalKeyThe evaluation key required to keyswitch/relinearize/rotate/automorphism operation in OpenFHE.

Results:

ResultDescription
outputA type for RLWE ciphertexts

openfhe.level_reduce (heir::openfhe::LevelReduceOp)

OpenFHE level_reduce operation of a ciphertext.

Syntax:

operation ::= `openfhe.level_reduce` operands attr-dict `:` functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
cryptoContextThe CryptoContext required to perform homomorphic operations in OpenFHE.
ciphertextA type for RLWE ciphertexts

Results:

ResultDescription
outputA type for RLWE ciphertexts

openfhe.mod_reduce (heir::openfhe::ModReduceOp)

OpenFHE mod_reduce operation of a ciphertext. (used only for BGV/CKKS)

Syntax:

operation ::= `openfhe.mod_reduce` operands attr-dict `:` functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
cryptoContextThe CryptoContext required to perform homomorphic operations in OpenFHE.
ciphertextA type for RLWE ciphertexts

Results:

ResultDescription
outputA type for RLWE ciphertexts

openfhe.mul_const (heir::openfhe::MulConstOp)

OpenFHE mul operation of a ciphertext and a constant.

Syntax:

operation ::= `openfhe.mul_const` operands attr-dict `:` functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
cryptoContextThe CryptoContext required to perform homomorphic operations in OpenFHE.
ciphertextA type for RLWE ciphertexts
constant64-bit signless integer

Results:

ResultDescription
outputA type for RLWE ciphertexts

openfhe.mul_no_relin (heir::openfhe::MulNoRelinOp)

OpenFHE mul operation of two ciphertexts without relinearization.

Syntax:

operation ::= `openfhe.mul_no_relin` operands attr-dict `:` functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
cryptoContextThe CryptoContext required to perform homomorphic operations in OpenFHE.
lhsA type for RLWE ciphertexts
rhsA type for RLWE ciphertexts

Results:

ResultDescription
outputA type for RLWE ciphertexts

openfhe.mul (heir::openfhe::MulOp)

OpenFHE mul operation of two ciphertexts with relinearization.

Syntax:

operation ::= `openfhe.mul` operands attr-dict `:` functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
cryptoContextThe CryptoContext required to perform homomorphic operations in OpenFHE.
lhsA type for RLWE ciphertexts
rhsA type for RLWE ciphertexts

Results:

ResultDescription
outputA type for RLWE ciphertexts

openfhe.mul_plain (heir::openfhe::MulPlainOp)

OpenFHE mul operation of a ciphertext and a plaintext.

Syntax:

operation ::= `openfhe.mul_plain` operands attr-dict `:` functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
cryptoContextThe CryptoContext required to perform homomorphic operations in OpenFHE.
ciphertextA type for RLWE ciphertexts
plaintextA type for RLWE plaintexts

Results:

ResultDescription
outputA type for RLWE ciphertexts

openfhe.negate (heir::openfhe::NegateOp)

OpenFHE negate operation of a ciphertext.

Syntax:

operation ::= `openfhe.negate` operands attr-dict `:` functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
cryptoContextThe CryptoContext required to perform homomorphic operations in OpenFHE.
ciphertextA type for RLWE ciphertexts

Results:

ResultDescription
outputA type for RLWE ciphertexts

openfhe.relin (heir::openfhe::RelinOp)

OpenFHE relinearize operation of a ciphertext.

Syntax:

operation ::= `openfhe.relin` operands attr-dict `:` functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
cryptoContextThe CryptoContext required to perform homomorphic operations in OpenFHE.
ciphertextA type for RLWE ciphertexts

Results:

ResultDescription
outputA type for RLWE ciphertexts

openfhe.rot (heir::openfhe::RotOp)

Syntax:

operation ::= `openfhe.rot` operands attr-dict `:` functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
index::mlir::IntegerAttrAn Attribute containing a integer value

Operands:

OperandDescription
cryptoContextThe CryptoContext required to perform homomorphic operations in OpenFHE.
ciphertextA type for RLWE ciphertexts

Results:

ResultDescription
outputA type for RLWE ciphertexts

openfhe.square (heir::openfhe::SquareOp)

OpenFHE square operation of a ciphertext.

Syntax:

operation ::= `openfhe.square` operands attr-dict `:` functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
cryptoContextThe CryptoContext required to perform homomorphic operations in OpenFHE.
ciphertextA type for RLWE ciphertexts

Results:

ResultDescription
outputA type for RLWE ciphertexts

openfhe.sub (heir::openfhe::SubOp)

OpenFHE sub operation of two ciphertexts.

Syntax:

operation ::= `openfhe.sub` operands attr-dict `:` functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
cryptoContextThe CryptoContext required to perform homomorphic operations in OpenFHE.
lhsA type for RLWE ciphertexts
rhsA type for RLWE ciphertexts

Results:

ResultDescription
outputA type for RLWE ciphertexts

3.8 - PolyExt

The PolyExt dialect is a place for “external” operations on mlir::polynomial::Polynomial.

PolyExt ops

poly_ext.cmod_switch (heir::poly_ext::CModSwitchOp)

Move polynomial into a ring with different cmod by scaling polynomial coefficients proportionally

Syntax:

operation ::= `poly_ext.cmod_switch` $x attr-dict `:` type($x) `->` type($output)

poly_ext.cmod_switch “moves” polynomial $x$ from ring $Z/QZ[X]/(f(x))$ to the ring $Z/qZ[X]/(f(x))$. The move is done by scaling the polynomial coefficients by $q/Q$ and “rounding” so input and output coefficients are congruent modulo congruence_modulus $t$. The resulting polynomial $\tilde{x} \approx (q/Q)x$ and $\tilde{x} \equiv x \mod t$.

Traits: AlwaysSpeculatableImplTrait, Elementwise, Scalarizable, Tensorizable, Vectorizable

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
congruence_modulus::mlir::IntegerAttrAn Attribute containing a integer value

Operands:

OperandDescription
xpolynomial-like

Results:

ResultDescription
outputpolynomial-like

3.9 - Polynomial

Polynomial additional definitions

3.10 - RNS

RNS types

RNSType

A residue number system representation

Syntax:

!rns.rns<
  ::llvm::ArrayRef<mlir::Type>   # basisTypes
>

Parameters:

ParameterC++ typeDescription
basisTypes::llvm::ArrayRef<mlir::Type>

RNS ops

RNS additional definitions

TypeInterface definitions

RNSBasisTypeInterface (RNSBasisTypeInterface)

This interface is required for a type to be used as a parameter to an rns type.

Methods:

isCompatibleWith

bool isCompatibleWith(::mlir::Type otherRnsBasisType);

Returns true if this type is compatible with another type in the same RNS basis. In particular, the set of types used for a single RNS basis are never equal as types, but instead have some common attribute that must be checked here. For example, an RNS type where the basis types are polynomials would return true if the two types are both polynomial types, even if they have different coefficient moduli.

isCompatibleWith must be both commutative and associative, in the sense that type1.isCompatibleWith(type2) if and only if type2.isCompatibleWith(type1), and further type2.isCompatibleWith(type3) if and only if type1.isCompatibleWith(type3).

NOTE: This method must be implemented by the user.

3.11 - Secret

Secret is a dialect for computations that operate on encrypted data.

Secret is intended to serve as a scheme-agnostic front-end for the HEIR ecosystem of dialects. It is supposed to be fully interoperable with the rest of MLIR via secret.generic, while lower-level HEIR dialects would have custom types for arithmetic on secret integers of various bit widths.

Secret types

SecretType

A secret value

Syntax:

!secret.secret<
  Type   # valueType
>

A generic wrapper around another MLIR type, representing an encrypted value but not specifying the manner of encryption. This is useful in HEIR because the compiler may choose various details of the FHE scheme based on the properties of the input program, the backend target hardware, and cost models of the various passes.

Parameters:

ParameterC++ typeDescription
valueTypeType

Secret ops

secret.cast (heir::secret::CastOp)

A placeholder cast from one secret type to another

Syntax:

operation ::= `secret.cast` $input attr-dict `:` qualified(type($input)) `to` qualified(type($output))

A cast operation represents a type cast from one secret type to another, that is used to enable the intermixing of various equivalent secret types before a lower-level FHE scheme has been chosen.

For example, secret.cast can be used to convert a secret<i8> to a secret<tensor<8xi1>> as a compatibility layer between boolean and non-boolean parts of a program. The pass that later lowers the IR to specific FHE schemes would need to replace these casts with appropriate scheme-specific operations, and it is left to those later passes to determine which casts are considered valid.

Example:

%result = secret.cast %0 : !secret.secret<i8> to !secret.secret<tensor<8xi1>>
%result2 = secret.cast %0 : !secret.secret<i8> to !secret.secret<tensor<2xi4>>

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
inputA secret value

Results:

ResultDescription
outputA secret value

secret.conceal (heir::secret::ConcealOp)

Convert a non-secret value into a secret

Syntax:

operation ::= `secret.conceal` $cleartext attr-dict `:` type($cleartext) `->` type($output)

Convert a value to a secret containing the same value.

This op represents a scheme-agnostic encryption operation, as well as a “trivial encryption” operation which is needed for some FHE schemes. This op is also useful for type materialization in the dialect conversion framework.

Examples:

%Y = secret.conceal %value : i32 -> !secret.secret<i32>

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
cleartextany type

Results:

ResultDescription
outputA secret value

secret.generic (heir::secret::GenericOp)

Lift a plaintext computation to operate on secrets.

secret.generic lifts a plaintext computation to operate on one or more secrets. The lifted computation is represented as a region containing a single block terminated by secret.yield. The arguments of the secret.generic may include one or more !secret.secret types. The arguments of the block in the op’s body correspond to the underlying plaintext types of the secrets.

secret.generic is not isolated from above, so you may directly reference values in the enclosing scope. This is required to support using secret.generic inside of ops with AffineScope, while having the body of the generic use the induction variables defined by the affine scope.

Basic examples:

Add two secret integers together

%Z = secret.generic ins(%X, %Y : !secret.secret<i32>, !secret.secret<i32>) {
  ^bb0(%x: i32, %y: i32):
    %z = arith.addi %x, %y: i32
    secret.yield %z : i32
  } -> (!secret.secret<i32>)

Add a secret value with a plaintext value. I.e., not all arguments to the op need be secret.

%Z = secret.generic ins(%X, %Y : i32, !secret.secret<i32>) {
  ^bb0(%x: i32, %y: i32):
    %z = arith.addi %x, %y: i32
    secret.yield %z : i32
  } -> (!secret.secret<i32>)

The same as above, but the plaintext op is not passed through the basic block.

%y = arith.constant 7: i32
%Z = secret.generic ins(%Y : !secret.secret<i32>) {
  ^bb0(%y: i32):
    %z = arith.addi %x, %y: i32
    secret.yield %z : i32
  } -> (!secret.secret<i32>)

Traits: SingleBlockImplicitTerminator<YieldOp>, SingleBlock

Operands:

OperandDescription
inputsvariadic of any type

Results:

ResultDescription
resultsvariadic of any type

secret.reveal (heir::secret::RevealOp)

Convert a secret value into a non-secret

Syntax:

operation ::= `secret.reveal` $input attr-dict `:` type($input) `->` type($cleartext)

Convert a secret into a non-secret containing the same value.

This op represents a scheme-agnostic decryption operation. This op is also useful for target materialization in the dialect conversion framework.

Examples:

%Y = secret.reveal %secret_value : !secret.secret<i32> -> i32

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
inputA secret value

Results:

ResultDescription
cleartextany type

secret.separator (heir::secret::SeparatorOp)

Convert a non-secret value into a secret

Syntax:

operation ::= `secret.separator` attr-dict ($inputs^ `:` type($inputs))?

This operation is used as a separation boundary between logical subunits of the module. This is used in conjunction with --secret-distribute-generic=distribute-through=secret.separator to break a generic around these separators and allow for optimization passses to analyze and optimize the sub-units locally.

In order to allow bufferization of modules with this operation, we must register a (bogus) memory effect that also prevents this operation from being trivially dead during operation folding.

This operation also accepts operands, which act as boundaries between the logical units. This enforces separation of memref and affine optimizations between the subunits, preventing optimizations from removing the operand and combining the two separated regions. The operand can be thought of as an return value of the logical subunit.

Interfaces: MemoryEffectOpInterface (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{MemoryEffects::Write on ::mlir::SideEffects::DefaultResource}

Operands:

OperandDescription
inputsvariadic of any type

secret.yield (heir::secret::YieldOp)

Secret yield operation

secret.yield is a special terminator operation for blocks inside regions in secret generic ops. It returns the cleartext value of the corresponding private computation to the immediately enclosing secret generic op.

Traits: AlwaysSpeculatableImplTrait, HasParent<GenericOp>, ReturnLike, Terminator

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface), RegionBranchTerminatorOpInterface

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
valuesvariadic of any type

3.12 - TensorExt

TensorExt attributes

SIMDPackingAttr

An attribute describing the SIMD packing of a tensor.

Syntax:

#tensor_ext.simd_packing<
  ::mlir::DenseI64ArrayAttr,   # in
  ::mlir::DenseI64ArrayAttr,   # padding
  ::mlir::DenseI64ArrayAttr,   # out
  int64_t   # padding_value
>

This attribute is used as the encoding attribute on a tensor. It describes the transformations that were applied to an input tensor to pack it into the given tensor.

The in attribute describes the shape of the original tensor. The following transformations are applied to the input tensor.

  1. Padding is applied first. The padding attribute is an array with the same size as the input tensor shape. Padding is applied at the end of the array using the padding_value attribute (default zero). The result after zero padding should be a power of two.

  2. The padded result is replicated or split to fill the output tensor shape.

For example,

#packing = #tensor_ext.simd_packing<
  in = [7],
  padding = [1],
  padding_value = 0,
  out = [16],
>

may be used on a tensor type like

tensor<1x16xi32, #packing>

If the original tensor had values [1, 2, 3, 4, 5, 6, 7] then a tensor with this attribute contains the data [1, 2, 3, 4, 5, 6, 7, 0, 1, 2, 3, 4, 5, 6, 7, 0].

Parameters:

ParameterC++ typeDescription
in::mlir::DenseI64ArrayAttr
padding::mlir::DenseI64ArrayAttr
out::mlir::DenseI64ArrayAttr
padding_valueint64_t

TensorExt ops

tensor_ext.rotate (heir::tensor_ext::RotateOp)

Rotate a tensor some number of indices left.

Syntax:

operation ::= `tensor_ext.rotate` operands attr-dict `:` qualified(type($tensor)) `,` type($shift)

This op represents a left-rotation of a tensor by given number of indices. Negative shift values are interpreted as right-rotations.

This corresponds to the rotate operation in arithmetic FHE schemes like BGV.

Examples:

%0 = ... : tensor<16xi32>
%c7 = arith.constant 7 : i32
%1 = tensor_ext.rotate %0, %c7 : tensor<16xi32>, i32

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
tensortensor of any type values
shiftsignless-integer-like

Results:

ResultDescription
outputtensor of any type values

3.13 - TfheRust

The thfe_rust dialect is an exit dialect for generating rust code against the tfhe-rs library API, using the shortint parameters and encoding scheme.

See https://github.com/zama-ai/tfhe-rs

TfheRust types

EncryptedInt8Type

An encrypted signed integer corresponding to tfhe-rs’s FHEInt8 type

Syntax: !tfhe_rust.ei8

EncryptedInt16Type

An encrypted signed integer corresponding to tfhe-rs’s FHEInt16 type

Syntax: !tfhe_rust.ei16

EncryptedInt32Type

An encrypted signed integer corresponding to tfhe-rs’s FHEInt32 type

Syntax: !tfhe_rust.ei32

EncryptedInt64Type

An encrypted signed integer corresponding to tfhe-rs’s FHEInt64 type

Syntax: !tfhe_rust.ei64

EncryptedInt128Type

An encrypted signed integer corresponding to tfhe-rs’s FHEInt128 type

Syntax: !tfhe_rust.ei128

EncryptedInt256Type

An encrypted signed integer corresponding to tfhe-rs’s FHEInt256 type

Syntax: !tfhe_rust.ei256

EncryptedUInt2Type

An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint2 type

Syntax: !tfhe_rust.eui2

EncryptedUInt3Type

An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint3 type

Syntax: !tfhe_rust.eui3

EncryptedUInt4Type

An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint4 type

Syntax: !tfhe_rust.eui4

EncryptedUInt8Type

An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint8 type

Syntax: !tfhe_rust.eui8

EncryptedUInt10Type

An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint10 type

Syntax: !tfhe_rust.eui10

EncryptedUInt12Type

An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint12 type

Syntax: !tfhe_rust.eui12

EncryptedUInt14Type

An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint14 type

Syntax: !tfhe_rust.eui14

EncryptedUInt16Type

An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint16 type

Syntax: !tfhe_rust.eui16

EncryptedUInt32Type

An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint32 type

Syntax: !tfhe_rust.eui32

EncryptedUInt64Type

An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint64 type

Syntax: !tfhe_rust.eui64

EncryptedUInt128Type

An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint128 type

Syntax: !tfhe_rust.eui128

EncryptedUInt256Type

An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint256 type

Syntax: !tfhe_rust.eui256

LookupTableType

A univariate lookup table used for programmable bootstrapping.

Syntax: !tfhe_rust.lookup_table

ServerKeyType

The short int server key required to perform homomorphic operations.

Syntax: !tfhe_rust.server_key

TfheRust ops

tfhe_rust.apply_lookup_table (heir::tfhe_rust::ApplyLookupTableOp)

Syntax:

operation ::= `tfhe_rust.apply_lookup_table` operands attr-dict `:` functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
serverKeyThe short int server key required to perform homomorphic operations.
inputAn encrypted unsigned integer corresponding to tfhe-rs’s FHEUint2 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint3 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint4 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint8 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint10 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint12 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint14 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint16 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint32 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint64 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint128 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint256 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt8 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt16 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt32 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt64 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt128 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt256 type
lookupTableA univariate lookup table used for programmable bootstrapping.

Results:

ResultDescription
outputAn encrypted unsigned integer corresponding to tfhe-rs’s FHEUint2 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint3 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint4 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint8 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint10 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint12 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint14 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint16 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint32 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint64 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint128 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint256 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt8 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt16 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt32 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt64 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt128 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt256 type

tfhe_rust.create_trivial (heir::tfhe_rust::CreateTrivialOp)

Syntax:

operation ::= `tfhe_rust.create_trivial` operands attr-dict `:` functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
serverKeyThe short int server key required to perform homomorphic operations.
valueinteger

Results:

ResultDescription
outputAn encrypted unsigned integer corresponding to tfhe-rs’s FHEUint2 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint3 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint4 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint8 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint10 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint12 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint14 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint16 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint32 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint64 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint128 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint256 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt8 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt16 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt32 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt64 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt128 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt256 type

tfhe_rust.generate_lookup_table (heir::tfhe_rust::GenerateLookupTableOp)

Syntax:

operation ::= `tfhe_rust.generate_lookup_table` operands attr-dict `:` functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Attributes:

AttributeMLIR TypeDescription
truthTable::mlir::IntegerAttrAn Attribute containing a integer value

Operands:

OperandDescription
serverKeyThe short int server key required to perform homomorphic operations.

Results:

ResultDescription
lookupTableA univariate lookup table used for programmable bootstrapping.

tfhe_rust.scalar_left_shift (heir::tfhe_rust::ScalarLeftShiftOp)

Syntax:

operation ::= `tfhe_rust.scalar_left_shift` operands attr-dict `:` functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
serverKeyThe short int server key required to perform homomorphic operations.
ciphertextAn encrypted unsigned integer corresponding to tfhe-rs’s FHEUint2 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint3 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint4 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint8 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint10 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint12 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint14 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint16 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint32 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint64 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint128 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint256 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt8 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt16 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt32 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt64 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt128 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt256 type
shiftAmount8-bit integer

Results:

ResultDescription
outputAn encrypted unsigned integer corresponding to tfhe-rs’s FHEUint2 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint3 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint4 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint8 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint10 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint12 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint14 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint16 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint32 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint64 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint128 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint256 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt8 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt16 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt32 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt64 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt128 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt256 type

tfhe_rust.add (heir::tfhe_rust::AddOp)

Arithmetic add of two tfhe ciphertexts.

Syntax:

operation ::= `tfhe_rust.add` operands attr-dict `:` functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
serverKeyThe short int server key required to perform homomorphic operations.
lhsAn encrypted unsigned integer corresponding to tfhe-rs’s FHEUint2 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint3 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint4 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint8 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint10 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint12 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint14 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint16 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint32 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint64 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint128 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint256 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt8 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt16 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt32 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt64 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt128 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt256 type
rhsAn encrypted unsigned integer corresponding to tfhe-rs’s FHEUint2 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint3 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint4 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint8 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint10 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint12 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint14 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint16 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint32 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint64 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint128 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint256 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt8 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt16 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt32 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt64 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt128 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt256 type

Results:

ResultDescription
outputAn encrypted unsigned integer corresponding to tfhe-rs’s FHEUint2 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint3 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint4 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint8 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint10 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint12 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint14 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint16 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint32 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint64 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint128 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint256 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt8 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt16 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt32 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt64 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt128 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt256 type

tfhe_rust.bitand (heir::tfhe_rust::BitAndOp)

Logical AND of two tfhe ciphertexts.

Syntax:

operation ::= `tfhe_rust.bitand` operands attr-dict `:` functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
serverKeyThe short int server key required to perform homomorphic operations.
lhsAn encrypted unsigned integer corresponding to tfhe-rs’s FHEUint2 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint3 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint4 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint8 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint10 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint12 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint14 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint16 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint32 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint64 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint128 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint256 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt8 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt16 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt32 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt64 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt128 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt256 type
rhsAn encrypted unsigned integer corresponding to tfhe-rs’s FHEUint2 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint3 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint4 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint8 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint10 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint12 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint14 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint16 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint32 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint64 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint128 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint256 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt8 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt16 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt32 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt64 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt128 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt256 type

Results:

ResultDescription
outputAn encrypted unsigned integer corresponding to tfhe-rs’s FHEUint2 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint3 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint4 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint8 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint10 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint12 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint14 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint16 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint32 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint64 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint128 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint256 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt8 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt16 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt32 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt64 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt128 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt256 type

tfhe_rust.sub (heir::tfhe_rust::SubOp)

Arithmetic sub of two tfhe ciphertexts.

Syntax:

operation ::= `tfhe_rust.sub` operands attr-dict `:` functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
serverKeyThe short int server key required to perform homomorphic operations.
lhsAn encrypted unsigned integer corresponding to tfhe-rs’s FHEUint2 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint3 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint4 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint8 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint10 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint12 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint14 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint16 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint32 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint64 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint128 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint256 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt8 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt16 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt32 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt64 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt128 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt256 type
rhsAn encrypted unsigned integer corresponding to tfhe-rs’s FHEUint2 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint3 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint4 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint8 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint10 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint12 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint14 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint16 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint32 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint64 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint128 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint256 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt8 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt16 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt32 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt64 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt128 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt256 type

Results:

ResultDescription
outputAn encrypted unsigned integer corresponding to tfhe-rs’s FHEUint2 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint3 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint4 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint8 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint10 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint12 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint14 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint16 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint32 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint64 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint128 type or An encrypted unsigned integer corresponding to tfhe-rs’s FHEUint256 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt8 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt16 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt32 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt64 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt128 type or An encrypted signed integer corresponding to tfhe-rs’s FHEInt256 type

3.14 - TfheRustBool

The tfhe_rust_bool dialect is an exit dialect for generating rust code against the tfhe-rs library API, using the boolean parameter set.

See https://github.com/zama-ai/tfhe-rs

TfheRustBool types

EncryptedBoolType

An encrypted Boolean corresponding to tfhe-rs’s FHEBool type

Syntax: !tfhe_rust_bool.eb

ServerKeyType

The boolean server key required to perform homomorphic operations.

Syntax: !tfhe_rust_bool.server_key

TfheRustBool ops

tfhe_rust_bool.and (heir::tfhe_rust_bool::AndOp)

Logical AND of two TFHE-rs Bool ciphertexts.

Syntax:

operation ::= `tfhe_rust_bool.and` operands attr-dict `:` functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
serverKeyThe boolean server key required to perform homomorphic operations.
lhseb-like
rhseb-like

Results:

ResultDescription
outputeb-like

tfhe_rust_bool.create_trivial (heir::tfhe_rust_bool::CreateTrivialOp)

Syntax:

operation ::= `tfhe_rust_bool.create_trivial` operands attr-dict `:` functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
serverKeyThe boolean server key required to perform homomorphic operations.
value1-bit signless integer

Results:

ResultDescription
outputAn encrypted Boolean corresponding to tfhe-rs’s FHEBool type

tfhe_rust_bool.mux (heir::tfhe_rust_bool::MuxOp)

Syntax:

operation ::= `tfhe_rust_bool.mux` operands attr-dict `:` functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
serverKeyThe boolean server key required to perform homomorphic operations.
cndAn encrypted Boolean corresponding to tfhe-rs’s FHEBool type
lhsAn encrypted Boolean corresponding to tfhe-rs’s FHEBool type
rhsAn encrypted Boolean corresponding to tfhe-rs’s FHEBool type

Results:

ResultDescription
outputAn encrypted Boolean corresponding to tfhe-rs’s FHEBool type

tfhe_rust_bool.nand (heir::tfhe_rust_bool::NandOp)

Logical NAND of two TFHE-rs Bool ciphertexts.

Syntax:

operation ::= `tfhe_rust_bool.nand` operands attr-dict `:` functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
serverKeyThe boolean server key required to perform homomorphic operations.
lhseb-like
rhseb-like

Results:

ResultDescription
outputeb-like

tfhe_rust_bool.nor (heir::tfhe_rust_bool::NorOp)

Logical NOR of two TFHE-rs Bool ciphertexts.

Syntax:

operation ::= `tfhe_rust_bool.nor` operands attr-dict `:` functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
serverKeyThe boolean server key required to perform homomorphic operations.
lhseb-like
rhseb-like

Results:

ResultDescription
outputeb-like

tfhe_rust_bool.not (heir::tfhe_rust_bool::NotOp)

Syntax:

operation ::= `tfhe_rust_bool.not` operands attr-dict `:` functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
serverKeyThe boolean server key required to perform homomorphic operations.
inputeb-like

Results:

ResultDescription
outputeb-like

tfhe_rust_bool.or (heir::tfhe_rust_bool::OrOp)

Logical OR of two TFHE-rs Bool ciphertexts.

Syntax:

operation ::= `tfhe_rust_bool.or` operands attr-dict `:` functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
serverKeyThe boolean server key required to perform homomorphic operations.
lhseb-like
rhseb-like

Results:

ResultDescription
outputeb-like

tfhe_rust_bool.xnor (heir::tfhe_rust_bool::XnorOp)

Logical XNOR of two TFHE-rs Bool ciphertexts.

Syntax:

operation ::= `tfhe_rust_bool.xnor` operands attr-dict `:` functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
serverKeyThe boolean server key required to perform homomorphic operations.
lhseb-like
rhseb-like

Results:

ResultDescription
outputeb-like

tfhe_rust_bool.xor (heir::tfhe_rust_bool::XorOp)

Logical XOR of two TFHE-rs Bool ciphertexts.

Syntax:

operation ::= `tfhe_rust_bool.xor` operands attr-dict `:` functional-type(operands, results)

Traits: AlwaysSpeculatableImplTrait

Interfaces: ConditionallySpeculatable, InferTypeOpInterface, NoMemoryEffect (MemoryEffectOpInterface)

Effects: MemoryEffects::Effect{}

Operands:

OperandDescription
serverKeyThe boolean server key required to perform homomorphic operations.
lhseb-like
rhseb-like

Results:

ResultDescription
outputeb-like

4 - Passes

This section contains the passes defined by HEIR.

4.1 - ApplyFoldersPasses

-apply-folders

Apply all folding patterns from canonicalize

This pass applies all registered folding patterns greedily to the input IR. This is useful when running a full canonicalize is too slow, but applying folders before canonicalize is sufficient to simplify the IR for later passes, or even sufficient to then subsequently run a full canonicalize pass.

This is used to prepare an IR for insert-rotate after fully unrolling loops.

4.2 - BGVPasses

-bgv-add-client-interface

Add client interfaces to BGV encrypted functions

This pass adds encrypt and decrypt functions for each compiled function in the IR. These functions maintain the same interface as the original function, while the compiled function may lose some of this information by the lowerings to ciphertext types (e.g., a scalar ciphertext, when lowered through BGV, must be encoded as a tensor).

Example:

For an input function with signature

#encoding = ...
#params = ...
!in_ty = !lwe.rlwe_ciphertext<encoding = #encoding, rlwe_params = #params, underlying_type = tensor<32xi16>>
!out_ty = !lwe.rlwe_ciphertext<encoding = #encoding, rlwe_params = #params, underlying_type = i16>
func.func @my_func(%arg0: !in_ty) -> !out_ty {
  ...
}

The pass will generate two new functions with signatures

func.func @my_func__encrypt(
  %arg0: tensor<32xi16>,
  %sk: !lwe.rlwe_secret_key<...>
) -> !in_ty

func.func @my_func__decrypt(
  %arg0: !out_ty,
  %sk: !lwe.rlwe_secret_key<...>
) -> i16

The my_func__encrypt function has the same order of operands as my_func, and uses their underylying_type as the corresponding input type. The last operand is the encryption key. The same holds for my_func__decrypt, but the inputs are the return types of my_func and the results are the underlying types of the return types of my_func.

If use-public-key is set to true, the encrypt function uses lwe.rlwe_public_key for encryption.

If one-value-per-helper-fn is set to true, the encryption helpers are split into separate functions, one for each SSA value being converted. For example, using the same !in_ty and !out_ty as above, this function signature

func.func @my_func(%arg0: !in_ty, %arg1: !in_ty) -> (!out_ty, !out_ty)

generates the following four helpers.

func.func @my_func__encrypt__arg0(%arg0: tensor<32xi16>, %sk: !lwe.rlwe_secret_key<...>) -> !in_ty
func.func @my_func__encrypt__arg1(%arg1: tensor<32xi16>, %sk: !lwe.rlwe_secret_key<...>) -> !in_ty
func.func @my_func__decrypt__result0(%arg0: !out_ty, %sk: !lwe.rlwe_secret_key<...>) -> i16
func.func @my_func__decrypt__result1(%arg1: !out_ty, %sk: !lwe.rlwe_secret_key<...>) -> i16
}

The suffix __argN indicates the SSA value being encrypted is the N-th argument of my_func, and similarly for __resultN.

Options

-use-public-key          : If true, generate a client interface that uses a public key for encryption.
-one-value-per-helper-fn : If true, split encryption helpers into separate functions for each SSA value.

4.3 - BGVToOpenfhe

-bgv-to-openfhe

Lower bgv to openfhe dialect.

This pass lowers the bgv dialect to Openfhe dialect.

4.4 - BGVToPolynomial

-bgv-to-polynomial

Lower bgv to polynomial dialect.

This pass lowers the bgv dialect to polynomial dialect.

4.5 - CGGIPasses

-cggi-set-default-parameters

Set default parameters for CGGI ops

This pass adds default parameters to all CGGI ops as cggi_params named attributes, overriding any existing attribute set with that name.

This pass is primarily for testing purposes, and as a parameter provider before a proper parameter selection mechanism is added. This pass should not be used in production.

The specific parameters are hard-coded in lib/Dialect/CGGI/Transforms/SetDefaultParameters.cpp.

4.6 - CGGIToTfheRust

-cggi-to-tfhe-rust

Lower cggi to tfhe_rust dialect.

4.7 - CGGIToTfheRustBool

-cggi-to-tfhe-rust-bool

Lower cggi to tfhe_rust_bool dialect.

4.8 - CombToCGGI

-comb-to-cggi

Lower comb to cggi dialect.

This pass lowers the comb dialect to cggi dialect.

4.9 - ConvertIfToSelectPasses

-convert-if-to-select

Convert scf.if operations on secret conditions to arith.select operations.

Conversion for If-operations that evaluate secret condition to alternative select operations.

4.10 - ElementwiseToAffinePasses

-convert-elementwise-to-affine

This pass lowers ElementwiseMappable operations to Affine loops.

This pass lowers ElementwiseMappable operations over tensors to affine loop nests that instead apply the operation to the underlying scalar values.

4.11 - ForwardStoreToLoadPasses

-forward-store-to-load

Forward stores to loads within a single block

This pass is a simplified version of mem2reg and similar passes. It analyzes an operation, finding all basic blocks within that op that have memrefs whose stores can be forwarded to loads.

Does not support complex control flow within a block, nor ops with arbitrary subregions.

4.12 - FullLoopUnrollPasses

-full-loop-unroll

Fully unroll all loops

Scan the IR for affine.for loops and unroll them all.

4.13 - LWEPasses

-lwe-set-default-parameters

Set default parameters for LWE ops

This pass adds default parameters to all lwe types as the lwe_params attribute, and for lwe ops as the params attribute, overriding any existing attributes set with those names.

This pass is primarily for testing purposes, and as a parameter provider before a proper parameter selection mechanism is added. This pass should not be used in production.

The specific parameters are hard-coded in lib/Dialect/LWE/Transforms/SetDefaultParameters.cpp.

4.14 - LWEToPolynomial

-lwe-to-polynomial

Lower lwe to polynomial dialect.

This pass lowers the lwe dialect to polynomial dialect.

4.15 - MemrefToArith

-expand-copy

Expands memref.copy ops to explicit affine loads and stores

This pass removes memref copy operations by expanding them to affine loads and stores. This pass introduces affine loops over the dimensions of the MemRef, so must be run prior to any affine loop unrolling in a pipeline.

Input

module {
  func.func @memref_copy() {
    %alloc = memref.alloc() : memref<2x3xi32>
    %alloc_0 = memref.alloc() : memref<2x3xi32>
    memref.copy %alloc, %alloc_0 : memref<1x1xi32> to memref<1x1xi32>
  }
}

Output

module {
  func.func @memref_copy() {
    %alloc = memref.alloc() : memref<2x3xi32>
    %alloc_0 = memref.alloc() : memref<2x3xi32>
    affine.for %arg0 = 0 to 2 {
      affine.for %arg1 = 0 to 3 {
        %1 = affine.load %alloc[%arg0, %arg1] : memref<2x3xi32>
        affine.store %1, %alloc_0[%arg0, %arg1] : memref<2x3xi32>
      }
    }
  }
}

When --disable-affine-loop=true is set, then the output becomes

module {
  func.func @memref_copy() {
    %alloc = memref.alloc() : memref<2x3xi32>
    %alloc_0 = memref.alloc() : memref<2x3xi32>
    %c0 = arith.constant 0 : index
    %c1 = arith.constant 1 : index
    %c2 = arith.constant 2 : index
    %0 = affine.load %alloc[%c0, %c0] : memref<2x3xi32>
    affine.store %0, %alloc_0[%c0, %c0] : memref<2x3xi32>
    %1 = affine.load %alloc[%c0, %c1] : memref<2x3xi32>
    affine.store %1, %alloc_0[%c0, %c1] : memref<2x3xi32>
    %2 = affine.load %alloc[%c0, %c2] : memref<2x3xi32>
    affine.store %2, %alloc_0[%c0, %c2] : memref<2x3xi32>
    [...]
  }
}

Options

-disable-affine-loop : Use this to control to disable using affine loops

-extract-loop-body

Extracts logic of a loop bodies into functions.

This pass extracts logic in the inner body of for loops into functions.

This pass requires that tensors are lowered to memref. It expects that a loop body contains a number of affine.load statements used as inputs to the extracted function, and a single affine.store used as the extracted function’s output.

Input

module {
  func.func @loop_body() {
    %c-128_i8 = arith.constant -128 : i8
    %c127_i8 = arith.constant 127 : i8
    %alloc_7 = memref.alloc() {alignment = 64 : i64} : memref<25x20x8xi8>
    affine.for %arg1 = 0 to 25 {
      affine.for %arg2 = 0 to 20 {
        affine.for %arg3 = 0 to 8 {
          %98 = affine.load %alloc_6[%arg1, %arg2, %arg3] : memref<25x20x8xi8>
          %99 = arith.cmpi slt, %arg0, %c-128_i8 : i8
          %100 = arith.select %99, %c-128_i8, %arg0 : i8
          %101 = arith.cmpi sgt, %arg0, %c127_i8 : i8
          %102 = arith.select %101, %c127_i8, %100 : i8
          affine.store %102, %alloc_7[%arg1, %arg2, %arg3] : memref<25x20x8xi8>
        }
      }
    }
  }
}

Output

module {
  func.func @loop_body() {
    %alloc_7 = memref.alloc() {alignment = 64 : i64} : memref<25x20x8xi8>
    affine.for %arg1 = 0 to 25 {
      affine.for %arg2 = 0 to 20 {
        affine.for %arg3 = 0 to 8 {
          %98 = affine.load %alloc_6[%arg1, %arg2, %arg3] : memref<25x20x8xi8>
          %102 = func.call @__for_loop(%98) : (i8) -> i8
          affine.store %102, %alloc_7[%arg1, %arg2, %arg3] : memref<25x20x8xi8>
        }
      }
    }
  }
  func.func private @__for_loop(%arg0: i8) -> i8 {
    %c-128_i8 = arith.constant -128 : i8
    %c127_i8 = arith.constant 127 : i8
    %99 = arith.cmpi slt, %arg0, %c-128_i8 : i8
    %100 = arith.select %99, %c-128_i8, %arg0 : i8
    %101 = arith.cmpi sgt, %arg0, %c127_i8 : i8
    %102 = arith.select %101, %c127_i8, %100 : i8
    return %102 : i8
  }
}

Options

-min-loop-size : Use this to control the minimum loop size to apply this pass
-min-body-size : Use this to control the minimum loop body size to apply this pass

-memref-global-replace

MemrefGlobalReplacePass forwards global memrefs accessors to arithmetic values

This pass forwards constant global MemRef values to referencing affine loads. This pass requires that the MemRef global values are initialized as constants and that the affine load access indices are constants (i.e. not variadic). Unroll affine loops prior to running this pass.

MemRef removal is required to remove any memory allocations from the input model (for example, TensorFlow models contain global memory holding model weights) to support FHE transpilation.

Input

module {
  memref.global "private" constant @__constant_8xi16 : memref<2x4xi16> = dense<[[-10, 20, 3, 4], [5, 6, 7, 8]]>
  func.func @main() -> i16 {
    %c1 = arith.constant 1 : index
    %c2 = arith.constant 2 : index
    %0 = memref.get_global @__constant_8xi16 : memref<2x4xi16>
    %1 = affine.load %0[%c1, %c1 + %c2] : memref<2x4xi16>
    return %1 : i16
  }
}

Output

module {
  func.func @main() -> i16 {
    %c1 = arith.constant 1 : index
    %c2 = arith.constant 2 : index
    %c8_i16 = arith.constant 8 : i16
    return %c8_i16 : i16
  }
}

-unroll-and-forward

Loop unrolls and forwards stores to loads.

This pass processes the first function in a given module, and, starting from the first loop, iteratively does the following:

  1. Fully unroll the loop.
  2. Scan for load ops. For each load op with a statically-inferrable access index:
  3. Backtrack to the original memref alloc
  4. Find all store ops at the corresponding index (possibly transitively through renames/subviews of the underlying alloc).
  5. Find the last store that occurs and forward it to the load.
  6. If the original memref is an input memref, then forward through any renames to make the target load load directly from the argument memref (instead of any subviews, say)
  7. Apply the same logic to any remaining loads not inside any for loop.

This pass requires that tensors are lowered to memref, and only supports affine loops with affine.load/store ops.

Memrefs that result from memref.get_global ops are excluded from forwarding, even if they are loaded with a static index, and are instead handled by memref-global-replace, which should be run after this pass.

4.16 - OpenfhePasses

-openfhe-configure-crypto-context

Configure the crypto context in OpenFHE

This pass generates helper functions to generate and configure the OpenFHE crypto context for the given function. Generating the crypto context sets the appropriate encryption parameters, while the configuration generates the necessary evaluation keys (relinearization and rotation keys).

For example, for an MLIR function @my_func, the generated helpers have the following signatures

func.func  @my_func__generate_crypto_context() -> !openfhe.crypto_context

func.func  @my_func__configure_crypto_context(!openfhe.crypto_context, !openfhe.private_key) -> !openfhe.crypto_context

Options

-entry-function : Default entry function name of entry function.

4.17 - PolynomialPasses

-convert-polynomial-mul-to-ntt

Rewrites polynomial operations to their NTT equivalents

Applies a rewrite pattern to convert polynomial multiplication to the equivalent using the number-theoretic transforms (NTT) when possible.

Polynomial multiplication can be rewritten as polynomial.NTT on each operand, followed by modulo elementwise multiplication of the point-value representation and then the inverse-NTT back to coefficient representation.

4.18 - PolynomialToStandard

-polynomial-to-standard

Lower polynomial to standard MLIR dialects.

This pass lowers the polynomial dialect to standard MLIR, a mixture of affine, tensor, and arith.

4.19 - SecretizePasses

-secretize

Adds secret argument attributes to entry function

Adds a secret.secret attribute argument to each argument in the entry function of an MLIR module. By default, the function is main. This may be overridden with the option -entry-function=top_level_func.

Options

-entry-function : entry function of the module

-wrap-generic

Wraps regions using secret args in secret.generic bodies

This pass wraps function regions of func.func that use secret arguments in secret.generic bodies.

Secret arguments are annotated using a secret.secret argument attribute. This pass converts these to secret types and then inserts a secret.generic body to hold the functions region. The output type is also converted to a secret.

Example input:

  func.func @main(%arg0: i32 {secret.secret}) -> i32 {
    %0 = arith.constant 100 : i32
    %1 = arith.addi %0, %arg0 : i32
    return %1 : i32
  }

Output:

  func.func @main(%arg0: !secret.secret<i32>) -> !secret.secret<i32> {
    %0 = secret.generic ins(%arg0 : !secret.secret<i32>) {
    ^bb0(%arg1: i32):
      %1 = arith.constant 100 : i32
      %2 = arith.addi %0, %arg1 : i32
      secret.yield %2 : i32
    } -> !secret.secret<i32>
    return %0 : !secret.secret<i32>
  }

4.20 - SecretPasses

-secret-capture-generic-ambient-scope

Capture the ambient scope used in a secret.generic

For each value used in the body of a secret.generic op, which is defined in the ambient scope outside the generic, add it to the argument list of the generic.

-secret-distribute-generic

Distribute generic ops through their bodies.

Converts generic ops whose region contains many ops into smaller sequences of generic ops whose regions contain a single op, dropping the generic part from any resulting generic ops that have no secret.secret inputs. If the op has associated regions, and the operands are not secret, then the generic is distributed recursively through the op’s regions as well.

This pass is intended to be used as part of a front-end pipeline, where a program that operates on a secret type annotates the input to a region as secret, and then wraps the contents of the region in a single large secret.generic, then uses this pass to simplify it.

The distribute-through option allows one to specify a comma-separated list of op names (e.g., distribute-thorugh="affine.for,scf.if"), which limits the distribution to only pass through those ops. If unset, all ops are distributed through when possible.

Options

-distribute-through : comma-separated list of ops that should be distributed through

-secret-extract-generic-body

Extract the bodies of all generic ops into functions

This pass extracts the body of all generic ops into functions, and replaces the generic bodies with call ops. Used as a sub-operation in some passes, and extracted into its own pass for testing purposes.

This pass works best when --secret-generic-absorb-constants is run before it so that the extracted function contains any constants used in the generic op’s body.

-secret-forget-secrets

Convert secret types to standard types

Drop the secret<...> type from the IR, replacing it with the contained type and the corresponding cleartext computation.

-secret-generic-absorb-constants

Copy constants into a secret.generic body

For each constant value used in the body of a secret.generic op, which is defined in the ambient scope outside the generic, add it’s definition into the generic body.

-secret-generic-absorb-dealloc

Copy deallocs of internal memrefs into a secret.generic body

For each memref allocated and used only within a body of a secret.generic op, add it’s dealloc of the memref into its generic body.

-secret-merge-adjacent-generics

Merge two adjacent generics into a single generic

This pass merges two immedaitely sequential generics into a single generic. Useful as a sub-operation in some passes, and extracted into its own pass for testing purposes.

4.21 - SecretToBGV

-secret-to-bgv

Lower secret to bgv dialect.

This pass lowers an IR with secret.generic blocks containing arithmetic operations to operations on ciphertexts with the BGV dialect.

The pass assumes that the secret.generic regions have been distributed through arithmetic operations so that only one ciphertext operation appears per generic block. It also requires that canonicalize was run so that non-secret values used are removed from the secret.generic’s block arguments.

The pass requires that all types are tensors of a uniform shape matching the dimension of the ciphertext space specified my poly-mod-degree.

Options

-poly-mod-degree      : Default degree of the cyclotomic polynomial modulus to use for ciphertext space.
-coefficient-mod-bits : Default number of bits of the prime coefficient modulus to use for the ciphertext space.

4.22 - StraightLineVectorizerPasses

-straight-line-vectorize

A vectorizer for straight line programs.

This pass ignores control flow and only vectorizes straight-line programs within a given region.

Options

-dialect : Use this to restrict the dialect whose ops should be vectorized.

4.23 - TensorExtPasses

-align-tensor-sizes

Resize tensors into tensors with a fixed size final dimension

This pass resizes input tensors with arbitrary sizes into tensors with whose final dimensions has a fixed size. All input tensors are required to be one-dimensional. The --size option specifies the size of the final dimension of the output tensors, and is required to be a power of two.

To align the tensors in the input IR, the pass first zero pads the input to the nearest power of two before replicating or splitting it into the output shape determined by size. The resulting transformation is described in a SIMDPackingAttr encoding attribute on the final tensor.

For example, with size=16,a tensor with 7 elements will be zero-padded to 8 elements, and then replicated twice to fill a tensor with size 16. The SIMDPackingAttr will encode the input shape, the number of elements that were zero-padded, and the output shape.

Input:

%0 = tensor.empty() : tensor<7xi32>

Output:

%0 = tensor.empty() : tensor<16xi32, #tensor_ext.simd_packing<in = [7], padding = [1], out = [16]>>

A tensor with 30 elements will be zero padded with 2 elements and split into two tensors of size 16.

Input:

%0 = tensor.empty() : tensor<30xi32>

Output:

%0 = tensor.empty() : tensor<2x16xi32, #tensor_ext.simd_packing<in = [30], padding = [2], out = [16]>>

Note that this pass does not insert any new operations like reshape, but rather transforms the IR to use tensors with a fixed dimension. This pass may be used to align the sizes of tensors that represent plaintexts and ciphertexts in RLWE schemes that support SIMD slots and operations.

Options

-size : Power of two size of the final dimension of the output tensors.

-collapse-insertion-chains

Collapse chains of extract/insert ops into rotate ops when possible

This pass is a cleanup pass for insert-rotate. That pass sometimes leaves behind a chain of insertion operations like this:

%extracted = tensor.extract %14[%c5] : tensor<16xi16>
%inserted = tensor.insert %extracted into %dest[%c0] : tensor<16xi16>
%extracted_0 = tensor.extract %14[%c6] : tensor<16xi16>
%inserted_1 = tensor.insert %extracted_0 into %inserted[%c1] : tensor<16xi16>
%extracted_2 = tensor.extract %14[%c7] : tensor<16xi16>
%inserted_3 = tensor.insert %extracted_2 into %inserted_1[%c2] : tensor<16xi16>
...
%extracted_28 = tensor.extract %14[%c4] : tensor<16xi16>
%inserted_29 = tensor.insert %extracted_28 into %inserted_27[%c15] : tensor<16xi16>
yield %inserted_29 : tensor<16xi16>

In many cases, this chain will insert into every index of the dest tensor, and the extracted values all come from consistently aligned indices of the same source tensor. In this case, the chain can be collapsed into a single rotate.

Each index used for insertion or extraction must be constant; this may require running --canonicalize or --sccp before this pass to apply folding rules (use --sccp if you need to fold constant through control flow).

-insert-rotate

Vectorize arithmetic FHE operations using HECO-style heuristics

This pass implements the SIMD-vectorization passes from the HECO paper.

The pass operates by identifying arithmetic operations that can be suitably combined into a combination of cyclic rotations and vectorized operations on tensors. It further identifies a suitable “slot target” for each operation and heuristically aligns the operations to reduce unnecessary rotations.

This pass by itself does not eliminate any operations, but instead inserts well-chosen rotations so that, for well-structured code (like unrolled affine loops), a subsequent --cse and --canonicalize pass will dramatically reduce the IR. As such, the pass is designed to be paired with the canonicalization patterns in tensor_ext, as well as the collapse-insertion-chains pass, which cleans up remaining insertion and extraction ops after the main simplifications are applied.

Unlike HECO, this pass operates on plaintext types and tensors, along with the HEIR-specific tensor_ext dialect for its cyclic rotate op. It is intended to be run before lowering to a scheme dialect like bgv.

-rotate-and-reduce

Use a logarithmic number of rotations to reduce a tensor.

This pass identifies when a commutative, associative binary operation is used to reduce all of the entries of a tensor to a single value, and optimizes the operations by using a logarithmic number of reduction operations.

In particular, this pass identifies an unrolled set of operations of the form (the binary ops may come in any order):

%0 = tensor.extract %t[0] : tensor<8xi32>
%1 = tensor.extract %t[1] : tensor<8xi32>
%2 = tensor.extract %t[2] : tensor<8xi32>
%3 = tensor.extract %t[3] : tensor<8xi32>
%4 = tensor.extract %t[4] : tensor<8xi32>
%5 = tensor.extract %t[5] : tensor<8xi32>
%6 = tensor.extract %t[6] : tensor<8xi32>
%7 = tensor.extract %t[7] : tensor<8xi32>
%8 = arith.addi %0, %1 : i32
%9 = arith.addi %8, %2 : i32
%10 = arith.addi %9, %3 : i32
%11 = arith.addi %10, %4 : i32
%12 = arith.addi %11, %5 : i32
%13 = arith.addi %12, %6 : i32
%14 = arith.addi %13, %7 : i32

and replaces it with a logarithmic number of rotate and addi operations:

%0 = tensor_ext.rotate %t, 4 : tensor<8xi32>
%1 = arith.addi %t, %0 : tensor<8xi32>
%2 = tensor_ext.rotate %1, 2 : tensor<8xi32>
%3 = arith.addi %1, %2 : tensor<8xi32>
%4 = tensor_ext.rotate %3, 1 : tensor<8xi32>
%5 = arith.addi %3, %4 : tensor<8xi32>

4.24 - UnusedMemRefPasses

-remove-unused-memref

Cleanup any unused memrefs

Scan the IR for unused memrefs and remove them.

This pass looks for locally allocated memrefs that are never used and deletes them. This pass can be used as a cleanup pass from other IR simplifications that forward stores to loads.

4.25 - YosysOptimizerPasses

-yosys-optimizer

Invoke Yosys to perform circuit optimization.

This pass invokes Yosys to convert an arithmetic circuit to an optimized boolean circuit that uses the arith and comb dialects.

Note that booleanization changes the function signature: multi-bit integers are transformed to a tensor of booleans, for example, an i8 is converted to tensor<8xi1>.

The optimizer will be applied to each secret.generic op containing arithmetic ops that can be optimized.

Optional parameters:

  • abc-fast: Run the abc optimizer in “fast” mode, getting faster compile time at the expense of a possibly larger output circuit.
  • unroll-factor: Before optimizing the circuit, unroll loops by a given factor. If unset, this pass will not unroll any loops.
  • print-stats: Prints statistics about the optimized circuits.
  • mode={Boolean,LUT}: Map gates to boolean gates or lookup table gates.

Statistics

total circuit size : The total circuit size for all optimized circuits, after optimization is done.

5 - Design

This section contains documentation on the high level design of the project. Readers are intended to have basic familiarity with MLIR and FHE.

5.1 - Secret

The secret dialect contains types and operations to represent generic computations on secret data. It is intended to be a high-level entry point for the HEIR compiler, agnostic of any particular FHE scheme.

Most prior FHE compiler projects design their IR around a specific FHE scheme, and provide dedicated IR types for the secret analogues of existing data types, and/or dedicated operations on secret data types. For example, the Concrete compiler has !FHE.eint<32> for an encrypted 32-bit integer, and add_eint and similar ops. HECO has !fhe.secret<T> that models a generic secret type, but similarly defines fhe.add and fhe.multiply, and other projects are similar.

The problem with this approach is that it is difficult to incorporate the apply upstream canonicalization and optimization passes to these ops. For example, the arith dialect in MLIR has canonicalization patterns that must be replicated to apply to FHE analogues. One of the goals of HEIR is to reuse as much upstream infrastructure as possible, and so this led us to design the secret dialect to have both generic types and generic computations. Thus, the secret dialect has two main parts: a secret<T> type that wraps any other MLIR type T, and a secret.generic op that lifts any computation on cleartext to the “corresponding” computation on secret data types.

Overview with BGV-style lowering pipeline

Here is an example of a program that uses secret to lift a dot product computation:

func.func @dot_product(
    %arg0: !secret.secret<tensor<8xi16>>,
    %arg1: !secret.secret<tensor<8xi16>>) -> !secret.secret<i16> {
  %c0_i16 = arith.constant 0 : i16
  %0 = secret.generic ins(%arg0, %arg1 : !secret.secret<tensor<8xi16>>, !secret.secret<tensor<8xi16>>) {
  ^bb0(%arg2: tensor<8xi16>, %arg3: tensor<8xi16>):
    %1 = affine.for %arg4 = 0 to 8 iter_args(%arg5 = %c0_i16) -> (i16) {
      %extracted = tensor.extract %arg2[%arg4] : tensor<8xi16>
      %extracted_0 = tensor.extract %arg3[%arg4] : tensor<8xi16>
      %2 = arith.muli %extracted, %extracted_0 : i16
      %3 = arith.addi %arg5, %2 : i16
      affine.yield %3 : i16
    }
    secret.yield %1 : i16
  } -> !secret.secret<i16>
  return %0 : !secret.secret<i16>
}

The operands to the generic op are the secret data types, and the op contains a single region, whose block arguments are the corresponding cleartext data values. Then the region is free to perform any computation, and the values passed to secret.yield are lifted back to secret types. Note that secret.generic is not isolated from its enclosing scope, so one may refer to cleartext SSA values without adding them as generic operands and block arguments.

Clearly secret.generic does not actually do anything. It is not decrypting data. It is merely describing the operation that one wishes to apply to the secret data in more familiar terms. It is a structural operation, primarily used to demarcate which operations involve secret operands and have secret results, and group them for later optimization. The benefit of this is that one can write optimization passes on types and ops that are not aware of secret, and they will naturally match on the bodies of generic ops.

For example, here is what the above dot product computation looks like after applying the -cse -canonicalize -heir-simd-vectorizer passes, the implementations of which do not depend on secret or generic.

func.func @dot_product(
    %arg0: !secret.secret<tensor<8xi16>>,
    %arg1: !secret.secret<tensor<8xi16>>) -> !secret.secret<i16> {
  %c1 = arith.constant 1 : index
  %c2 = arith.constant 2 : index
  %c4 = arith.constant 4 : index
  %c7 = arith.constant 7 : index
  %0 = secret.generic ins(%arg0, %arg1 : !secret.secret<tensor<8xi16>>, !secret.secret<tensor<8xi16>>) {
  ^bb0(%arg2: tensor<8xi16>, %arg3: tensor<8xi16>):
    %1 = arith.muli %arg2, %arg3 : tensor<8xi16>
    %2 = tensor_ext.rotate %1, %c4 : tensor<8xi16>, index
    %3 = arith.addi %1, %2 : tensor<8xi16>
    %4 = tensor_ext.rotate %3, %c2 : tensor<8xi16>, index
    %5 = arith.addi %3, %4 : tensor<8xi16>
    %6 = tensor_ext.rotate %5, %c1 : tensor<8xi16>, index
    %7 = arith.addi %5, %6 : tensor<8xi16>
    %extracted = tensor.extract %7[%c7] : tensor<8xi16>
    secret.yield %extracted : i16
  } -> !secret.secret<i16>
  return %0 : !secret.secret<i16>
}

The canonicalization patterns for secret.generic apply a variety of simplifications, such as:

  • Removing any unused or non-secret arguments and return values.
  • Hoisting operations in the body of a generic that only depend on cleartext values to the enclosing scope.
  • Removing any generic ops that use no secrets at all.

These can be used together with the secret-distribute-generic pass to split an IR that contains a large generic op into generic ops that contain a single op, which can then be lowered to a particular FHE scheme dialect with dedicated ops. This makes lowering easier because it gives direct access to the secret version of each type that is used as input to an individual op.

As an example, a single-op secret might look like this (taken from the larger example below. Note the use of a cleartext from the enclosing scope, and the proximity of the secret type to the op to be lowered.

  %c2 = arith.constant 2 : index
  %3 = secret.generic ins(%2 : !secret.secret<tensor<8xi16>>) {
  ^bb0(%arg2: tensor<8xi16>):
    %8 = tensor_ext.rotate %arg2, %c2 : tensor<8xi16>, index
    secret.yield %8 : tensor<8xi16>
  } -> !secret.secret<tensor<8xi16>>

For a larger example, applying --secret-distribute-generic --canonicalize to the IR above:

func.func @dot_product(%arg0: !secret.secret<tensor<8xi16>>, %arg1: !secret.secret<tensor<8xi16>>) -> !secret.secret<i16> {
  %c1 = arith.constant 1 : index
  %c2 = arith.constant 2 : index
  %c4 = arith.constant 4 : index
  %c7 = arith.constant 7 : index
  %0 = secret.generic ins(%arg0, %arg1 : !secret.secret<tensor<8xi16>>, !secret.secret<tensor<8xi16>>) {
  ^bb0(%arg2: tensor<8xi16>, %arg3: tensor<8xi16>):
    %8 = arith.muli %arg2, %arg3 : tensor<8xi16>
    secret.yield %8 : tensor<8xi16>
  } -> !secret.secret<tensor<8xi16>>
  %1 = secret.generic ins(%0 : !secret.secret<tensor<8xi16>>) {
  ^bb0(%arg2: tensor<8xi16>):
    %8 = tensor_ext.rotate %arg2, %c4 : tensor<8xi16>, index
    secret.yield %8 : tensor<8xi16>
  } -> !secret.secret<tensor<8xi16>>
  %2 = secret.generic ins(%0, %1 : !secret.secret<tensor<8xi16>>, !secret.secret<tensor<8xi16>>) {
  ^bb0(%arg2: tensor<8xi16>, %arg3: tensor<8xi16>):
    %8 = arith.addi %arg2, %arg3 : tensor<8xi16>
    secret.yield %8 : tensor<8xi16>
  } -> !secret.secret<tensor<8xi16>>
  %3 = secret.generic ins(%2 : !secret.secret<tensor<8xi16>>) {
  ^bb0(%arg2: tensor<8xi16>):
    %8 = tensor_ext.rotate %arg2, %c2 : tensor<8xi16>, index
    secret.yield %8 : tensor<8xi16>
  } -> !secret.secret<tensor<8xi16>>
  %4 = secret.generic ins(%2, %3 : !secret.secret<tensor<8xi16>>, !secret.secret<tensor<8xi16>>) {
  ^bb0(%arg2: tensor<8xi16>, %arg3: tensor<8xi16>):
    %8 = arith.addi %arg2, %arg3 : tensor<8xi16>
    secret.yield %8 : tensor<8xi16>
  } -> !secret.secret<tensor<8xi16>>
  %5 = secret.generic ins(%4 : !secret.secret<tensor<8xi16>>) {
  ^bb0(%arg2: tensor<8xi16>):
    %8 = tensor_ext.rotate %arg2, %c1 : tensor<8xi16>, index
    secret.yield %8 : tensor<8xi16>
  } -> !secret.secret<tensor<8xi16>>
  %6 = secret.generic ins(%4, %5 : !secret.secret<tensor<8xi16>>, !secret.secret<tensor<8xi16>>) {
  ^bb0(%arg2: tensor<8xi16>, %arg3: tensor<8xi16>):
    %8 = arith.addi %arg2, %arg3 : tensor<8xi16>
    secret.yield %8 : tensor<8xi16>
  } -> !secret.secret<tensor<8xi16>>
  %7 = secret.generic ins(%6 : !secret.secret<tensor<8xi16>>) {
  ^bb0(%arg2: tensor<8xi16>):
    %extracted = tensor.extract %arg2[%c7] : tensor<8xi16>
    secret.yield %extracted : i16
  } -> !secret.secret<i16>
  return %7 : !secret.secret<i16>
}

And then lowering it to bgv with --secret-to-bgv="poly-mod-degree=8" (the pass option matches the tensor size, but it is an unrealistic FHE polynomial degree used here just for demonstration purposes). Note type annotations on ops are omitted for brevity.

#encoding = #lwe.polynomial_evaluation_encoding<cleartext_start = 16, cleartext_bitwidth = 16>
#params = #lwe.rlwe_params<ring = <cmod=463187969, ideal=#_polynomial.polynomial<1 + x**8>>>
!ty1 = !lwe.rlwe_ciphertext<encoding=#encoding, rlwe_params=#params, underlying_type=tensor<8xi16>>
!ty2 = !lwe.rlwe_ciphertext<encoding=#encoding, rlwe_params=#params, underlying_type=i16>

func.func @dot_product(%arg0: !ty1, %arg1: !ty1) -> !ty2 {
  %c1 = arith.constant 1 : index
  %c2 = arith.constant 2 : index
  %c4 = arith.constant 4 : index
  %c7 = arith.constant 7 : index
  %0 = bgv.mul %arg0, %arg1
  %1 = bgv.relinearize %0 {from_basis = array<i32: 0, 1, 2>, to_basis = array<i32: 0, 1>}
  %2 = bgv.rotate %1, %c4
  %3 = bgv.add %1, %2
  %4 = bgv.rotate %3, %c2
  %5 = bgv.add %3, %4
  %6 = bgv.rotate %5, %c1
  %7 = bgv.add %5, %6
  %8 = bgv.extract %7, %c7
  return %8
}

Differences for CGGI-style pipeline

The tosa-to-boolean-tfhe and related pipelines add a few additional steps. The main goal here is to apply a hardware circuit optimizer to blocks of standard MLIR code (inside secret.generic ops) which converts the computation to an optimized boolean circuit with a desired set of gates. Only then is -secret-distribute-generic applied to split the ops up and lower them to the cggi dialect. In particular, because passing an IR through the circuit optimizer requires unrolling all loops, one useful thing you might want to do is to optimize only the body of a for loop nest.

To accomplish this, we have two additional mechanisms. One is the pass option ops-to-distribute for -secret-distribute-generic, which allows the user to specify a list of ops that generic should be split across, and all others left alone. Specifying affine.for here will pass generic through the affine.for loop, but leave its body intact. This can also be used with the -unroll-factor option to the -yosys-optimizer pass to partially unroll a loop nest and pass the partially-unrolled body through the circuit optimizer.

The other mechanism is the secret.separator op, which is a purely structural op that demarcates the boundary of a subset of a block that should be jointly optimized in the circuit optimizer.

For example, the following tosa ops lower to multiple linalg instructions, and hence multiple for loops, that we want to pass to a circuit optimizer as a unit. The secret.separator ops surrounding the op are preserved through the lowering.

func.func @main(%arg0: tensor<1x1xi8> {secret.secret}) -> tensor<1x16xi32> {
  secret.separator
  %4 = "tosa.const"() {value = dense<[0, 0, -5438, -5515, -1352, -1500, -4152, -84, 3396, 0, 1981, -5581, 0, -6964, 3407, -7217]> : tensor<16xi32>} : () -> tensor<16xi32>
  %5 = "tosa.const"() {value = dense<[[-9], [-54], [57], [71], [104], [115], [98], [99], [64], [-26], [127], [25], [-82], [68], [95], [86]]> : tensor<16x1xi8>} : () -> tensor<16x1xi8>
  %6 = "tosa.fully_connected"(%arg0, %5, %4) {quantization_info = #tosa.conv_quant<input_zp = -128, weight_zp = 0>} : (tensor<1x1xi8>, tensor<16x1xi8>, tensor<16xi32>) -> tensor<1x16xi32>
  secret.separator
  return %6 : tensor<1x16xi32>
}

After running --tosa-to-boolean-tfhe and dumping the IR after the linalg ops are lowered to loops, we can see the secret.separator ops enclose the lowered ops, with the exception of some pure ops that are speculatively executed.

func.func @main(%arg0: memref<1x1xi8, strided<[?, ?], offset: ?>> {secret.secret}) -> memref<1x16xi32> {
  %c-128_i32 = arith.constant -128 : i32
  %0 = memref.get_global @__constant_16xi32 : memref<16xi32>
  %1 = memref.get_global @__constant_16x1xi8 : memref<16x1xi8>
  secret.separator
  %alloc = memref.alloc() {alignment = 64 : i64} : memref<1x16xi8>
  affine.for %arg1 = 0 to 1 {
    affine.for %arg2 = 0 to 16 {
      %2 = affine.load %1[%arg2, %arg1] : memref<16x1xi8>
      affine.store %2, %alloc[%arg1, %arg2] : memref<1x16xi8>
    }
  }
  %alloc_0 = memref.alloc() {alignment = 64 : i64} : memref<1x16xi32>
  affine.for %arg1 = 0 to 1 {
    affine.for %arg2 = 0 to 16 {
      %2 = affine.load %0[%arg2] : memref<16xi32>
      affine.store %2, %alloc_0[%arg1, %arg2] : memref<1x16xi32>
    }
  }
  affine.for %arg1 = 0 to 1 {
    affine.for %arg2 = 0 to 16 {
      affine.for %arg3 = 0 to 1 {
        %2 = affine.load %arg0[%arg1, %arg3] : memref<1x1xi8, strided<[?, ?], offset: ?>>
        %3 = affine.load %alloc[%arg3, %arg2] : memref<1x16xi8>
        %4 = affine.load %alloc_0[%arg1, %arg2] : memref<1x16xi32>
        %5 = arith.extsi %2 : i8 to i32
        %6 = arith.subi %5, %c-128_i32 : i32
        %7 = arith.extsi %3 : i8 to i32
        %8 = arith.muli %6, %7 : i32
        %9 = arith.addi %4, %8 : i32
        affine.store %9, %alloc_0[%arg1, %arg2] : memref<1x16xi32>
      }
    }
  }
  secret.separator
  memref.dealloc %alloc : memref<1x16xi8>
  return %alloc_0 : memref<1x16xi32>
}

We decided to use the separator op over a few alternatives:

  • Grouping by secret.generic: these tosa ops must be bufferized, but secret types cannot participate in bufferization (see the Limitations section).
  • Grouping by basic blocks: secret.generic is a single-block op with a yield terminator, and grouping by blocks would require us to change this.
  • Grouping by regions: SSA values generated by a region are not visible to the enclosing scope, so we would need to have the region-bearing op return values, which is tedious to organize.
  • Attaching attributes to ops that should be grouped together: this would not be preserved by upstream lowerings and optimization passes.

generic operands

secret.generic takes any SSA values as legal operands. They may be secret types or non-secret. Canonicalizing secret.generic removes non-secret operands and leaves them to be referenced via the enclosing scope (secret.generic is not IsolatedFromAbove).

This may be unintuitive, as one might expect that only secret types are valid arguments to secret.generic, and that a verifier might assert non-secret args are not present.

However, we allow non-secret operands because it provides a convenient scope encapsulation mechanism, which is useful for the --yosys-optimizer pass that runs a circuit optimizer on individual secret.generic ops and needs to have access to all SSA values used as inputs. The following passes are related to this functionality:

  • secret-capture-generic-ambient-scope
  • secret-generic-absorb-constants
  • secret-extract-generic-body

Due to the canonicalization rules for secret.generic, anyone using these passes as an IR organization mechanism must be sure not to canonicalize before accomplishing the intended task.

Limitations

Bufferization

Secret types cannot participate in bufferization passes. In particular, -one-shot-bufferize hard-codes the notion of tensor and memref types, and so it cannot currently operate on secret<tensor<...>> or secret<memref<...>> types, which prevents us from implementing a bufferization interface for secret.generic. This was part of the motivation to introduce secret.separator, because tosa ops like a fully connected neural network layer lower to multiple linalg ops, and these ops need to be bufferized before they can be lowered further. However, we want to keep the lowered ops grouped together for circuit optimization (e.g., fusing transposes and constant weights into the optimized layer), but because of this limitation, we can’t simply wrap the tosa ops in a secret.generic (bufferization would fail).

5.2 - SIMD Optimizations

HEIR includes a SIMD (Single Instruction, Multiple Data) optimizer which is designed to exploit the restricted SIMD parallelism most (Ring-LWE-based) FHE schemes support (also commonly known as “packing” or “batching”). Specifically, HEIR incorporates the “automated batching” optimizations (among many other things) from the HECO compiler. The following will assume basic familiarity with the FHE SIMD paradigm and the high-level goals of the optimization, and we refer to the associated HECO paper, slides, talk and additional resources on the Usenix'23 website for an introduction to the topic. This documentation will mostly focus on describing how the optimization is realized in HEIR (which differs somewhat from the original implementation) and how the optimization is intended to be used in an overall end-to-end compilation pipeline.

Representing FHE SIMD Operations

Following the design principle of maintaining programs in standard MLIR dialects as long as possible (cf. the design rationale behind the Secret Dialect), HEIR uses the MLIR tensor dialect and ElementwiseMappable operations from the MLIR arith dialect to represent HE SIMD operations.

We do introduce the HEIR-specific tensor_ext.rotate operation, which represents a cyclical left-rotation of a tensor. Note that, as the current SIMD vectorizer only supports one-dimensional tensors, the semantics of this operation on multi-dimensional tensors are not (currently) defined.

For example, the common “rotate-and-reduce” pattern which results in each element containing the sum/product/etc of the original vector can be expressed as:

%tensor = tensor.from_elements %i1, %i2, %i3, %i4, %i5, %i6, %i7, %i8 : tensor<8xi16>
%0 = tensor_ext.rotate %tensor, %c4 : tensor<8xi16>, index
%1 = arith.addi %tensor, %0 : tensor<8xi16>
%2 = tensor_ext.rotate %1, %c2 : tensor<8xi16>, index
%3 = arith.addi %1, %2 : tensor<8xi16>
%4 = tensor_ext.rotate %3, %c1 : tensor<8xi16>, index
%5 = arith.addi %3, %4 : tensor<8xi16>

The %cN and %iN, which are defined as %cN = arith.constant N : index and %iN = arith.constant N : i16, respectively, have been omitted for readability.

Intended Usage

The -heir-simd-vectorizer pipeline transforms a program consisting of loops and index-based accesses into tensors (e.g., tensor.extract and tensor.insert) into one consisting of SIMD operations (including rotations) on entire tensors. While its implementation does not depend on any FHE-specific details or even the Secret dialect, this transformation is likely only useful when lowering a high-level program to an arithmetic-circuit-based FHE scheme (e.g., B/FV, BGV, or CKKS). The -mlir-to-openfhe-bgv pipeline demonstrates the intended flow: augmenting a high-level program with secret annotations, then applying the SIMD optimization (and any other high-level optimizations) before lowering to BGV operations and then exiting to OpenFHE.

Warning The current SIMD vectorizer pipeline supports only one-dimensional tensors. As a workaround, one could reshape all multi-dimensional tensors into one-dimensional tensors, but MLIR/HEIR currently do not provide a pass to automate this process.

Since the optimization is based on heuristics, the resulting program might not be optimal or could even be worse than a trivial realization that does not use ciphertext packing. However, well-structured programs generally lower to reasonable batched solutions, even if they do not achieve optimal batching layouts. For common operations such as matrix-vector or matrix-matrix multiplications, state-of-the-art approaches require advanced packing schemes that might map elements into the ciphertext vector in non-trivial ways (e.g., diagonal-major and/or replicated). The current SIMD vectorizer will never change the arrangement of elements inside an input tensor and therefore cannot produce the optimal approaches for these operations.

Note, that the SIMD batching optimization is different from, and significantly more complex than, the Straight Line Vectorizer (-straight-line-vectorize pass), which simply groups ElementwiseMappable operations that agree in operation name and operand/result types into vectorized/tensorized versions.

Implementation

Below, we give a brief overview over the implementation, with the goal of both improving maintainability/extensibility of the SIMD vectorizer and allowing advanced users to better understand why a certain program is transformed in the way it is.

Components

The -heir-simd-vectorizer pipeline uses a combination of standard MLIR passes (-canonicalize, -cse, -sccp) and custom HEIR passes. Some of these (-apply-folders, -full-loop-unroll) might have applications outside the SIMD optimization, while others (-insert-rotate, -collapse-insertion-chains and -rotate-and-reduce) are very specific to the FHE SIMD optimization. In addition, the passes make use of the RotationAnalysis and TargetSlotAnalysis analyses.

High-Level Flow

  • Loop Unrolling (-full-loop-unroll): The implementation currently begins by unrolling all loops in the program to simplify the later passes. See #589 for a discussion on how this could be avoided.

  • Canonicalization (-apply-folders -canonicalize): As the rotation-specific passes are very strict about the structure of the IR they operate on, we must first simplify away things such as tensors of constant values. For performance reasons (c.f. comments in the heirSIMDVectorizerPipelineBuilder function in heir-opt.cpp), this must be done by first applying folds before applying the full canonicalization.

  • Main SIMD Rewrite (-insert-rotate -cse -canonicalize -cse): This pass rewrites arithmetic operations over tensor.extract-ed operands into SIMD operations over the entire tensor, rotating the (full-tensor) operands so that the correct elements interact. For example, it will rewrite the following snippet (which computes t2[4] = t0[3] + t1[5])

    %0 = tensor.extract %t0[%c3] : tensor<32xi16>
    %1 = tensor.extract %t1[%c5] : tensor<32xi16>
    %2 = arith.addi %0, %1 : i16
    %3 = tensor.insert %2 into %t2[%c4] : tensor<32xi16>
    

    to

    %0 = tensor_ext.rotate %t0, %c31 : tensor<32xi16>, index
    %1 = tensor_ext.rotate %t1, %c1 : tensor<32xi16>, index
    %2 = arith.addi %0, %1 : tensor<32xi16>
    

    i.e., rotating t0 down by one (31 = -1 (mod 32)) and t1 up by one to bring the elements at index 3 and 5, respectively, to the “target” index 4. The pass uses the TargetSlotAnalysis to identify the appropriate target index (or ciphertext “slot” in FHE-speak). See Insert Rotate Pass below for more details. This pass is roughly equivalent to the -batching pass in the original HECO implementation.

    Doing this rewrite by itself does not represent an optimization, but if we consider what happens to the corresponding code for other indices (e.g., t2[5] = t0[4] + t1[6]), we see that the pass transforms expressions with the same relative index offsets into the exact same set of rotations/SIMD operations, so the following Common Subexpression Elimination (CSE) will remove redundant computations. We apply CSE twice, once directly (which creates new opportunities for canonicalization and folding) and then again after that canonicalization. See TensorExt Canonicalization for a description of the rotation-specific canonocalization patterns).

  • Cleanup of Redundant Insert/Extract (-collapse-insertion-chains -sccp -canonicalize -cse): Because the -insert-rotate pass maintains the consistency of the IR, it emits a tensor.extract operation after the SIMD operation and uses that to replace the original operation (which is valid, as both produce the desired scalar result). As a consequence, the generated code for the snippet above is actually trailed by a (redundant) extract/insert:

    %extracted = tensor.extract %2[%c4] : tensor<32xi16>
    %inserted = tensor.insert %extracted into %t2[%c4] : tensor<32xi16>
    

    In real code, this might generate a long series of such extraction/insertion operations, all extracting from the same (due to CSE) tensor and inserting into the same output tensor. Therefore, the -collapse-insertion-chains pass searches for such chains over entire tensors and collapses them. It supports not just chains where the indices match perfectly, but any chain where the relative offset is consistent across the tensor, issuing a rotation to realize the offset (if the offset is zero, the canonicalization will remove the redundant rotation). Note, that in HECO, insertion/extraction is handled differently, as HECO features a combine operation modelling not just simple insertions (combine(%t0#j, %t1)) but also more complex operations over slices of tensors (combine(%t0#[i,j], %t1)). As a result, the equivalent pass in HECO (-combine-simplify) instead joins different combine operations, and a later fold removes combines that replace the entire target tensor. See issue #512 for a discussion on why the combine operation is a more powerful framework and what would be necessary to port it to HEIR.

  • Applying Rotate-and-Reduce Patterns (-rotate-and-reduce -sccp -canonicalize -cse): The rotate and reduce pattern (see Representing FHE SIMD Operations for an example) is an important aspect of accelerating SIMD-style operations in FHE, but it does not follow automatically from the batching rewrites applied so far. As a result, the -rotate-and-reduce pass needs to search for sequences of arithmetic operations that correspond to the full folding of a tensor, i.e., patterns such as t[0]+(t[1]+(t[2]+t[3]+(...))), which currently uses a backwards search through the IR, but could be achieved more efficiently through a data flow analysis (c.f. issue #532). In HECO, rotate-and-reduce is handled differently, by identifying sequences of compatible operations prior to batching and rewriting them to “n-ary” operations. However, this approach requires non-standard arithmetic operations and is therefore not suitable for use in HEIR. However, there is likely still an opportunity to make the patterns in HEIR more robust/general (e.g., support constant scalar operands in the fold, or support non-full-tensor folds). See issue #522 for ideas on how to make the HEIR pattern more robust/more general.

Insert Rotate Pass

TODO(#721): Write a detailed description of the rotation insertion pass and the associated target slot analysis.

TensorExt Canonicalization

The TensorExt (tensor_ext) Dialect includes a series of canonicalization rules that are essential to making automatically generated rotation code efficient:

  • Rotation by zero: rotate %t, 0 folds away to %t

  • Cyclical wraparound: rotate %t, k for $k > t.size$ can be simplified to rotate %t, (k mod t.size)

  • Sequential rotation: %0 = rotate %t, k followed by %1 = rotate %0, l is simplified to rotate %t (k+l)

  • Extraction: %0 = rotate %t, k followed by %1 = tensor.extract %0[l] is simplified to tensor.extract %t[k+l]

  • Binary Arithmetic Ops: where both operands to a binary arith operation are rotations by the same amount, the rotation can be performed only once, on the result. For Example,

    %0 = rotate %t1, k
    %1 = rotate %t2, k
    %2 = arith.add %0, %1
    

    can be simplified to

    %0 = arith.add %t1, %t2
    %1 = rotate %0, k
    
  • Sandwiched Binary Arithmetic Ops: If a rotation follows a binary arith operation which has rotation as its operands, the post-arith operation can be moved forward. For example,

    %0 = rotate %t1, x
    %1 = rotate %t2, y
    %2 = arith.add %0, %1
    %3 = rotate %2, z
    

    can be simplified to

    %0 = rotate %t1, x + z
    %1 = rotate %t2, y + z
    %2 = arith.add %0, %1
    
  • Single-Use Arithmetic Ops: Finally, there is a pair of rules that do not eliminate rotations, but move rotations up in the IR, which can help in exposing further canonicalization and/or CSE opportunities. These only apply to arith operations with a single use, as they might otherwise increase the total number of rotations. For example,

    %0 = rotate %t1, k
    %2 = arith.add %0, %t2
    %1 = rotate %2, l
    

    can be equivalently rewritten as

    %0 = rotate %t1, (k+l)
    %1 = rotate %t2, l
    %2 = arith.add %0, %1
    

    and a similar pattern exists for situations where the rotation is the rhs operand of the arithmetic operation.

Note that the index computations in the patterns above (e.g., k+l, k mod t.size are realized via emitting arith operations. However, for constant/compile-time-known indices, these will be subsequently constant-folded away by the canonicalization pass.

6 - Pipelines

heir-opt

--heir-simd-vectorizer

Run scheme-agnostic passes to convert FHE programs that operate on scalar types to equivalent programs that operate on vectors.

This pass is intended to process FHE programs that are known to be good for SIMD, but a specific FHE scheme has not yet been chosen. It expects to handle arith ops operating on tensor types (with or without secret.generic).

The pass unrolls all loops, then applies a series of passes that convert scalar operations on tensor elements to SIMD operations on full tensors. This uses the FHE computational model common to BGV, BFV, and CKKS, in which data is packed in polynomial ciphertexts, interpreted as vectors of individual data elements, and arithmetic can be applied across entire ciphertexts, with some limited support for rotations via automorphisms of the underlying ring.

Along the way, this pipeline applies heuristic optimizations to minimize the number of rotations needed, relying on the implicit cost model that rotations are generally expensive. The specific set of passes can be found in tools/heir-opt.cpp where the pipeline is defined.

--heir-tosa-to-arith

Lowers a TOSA MLIR model to func, arith, and memref.

Lowers from TOSA through linalg and affine, and converts all tensors to memrefs. Fully unrolls all loops, and forwards stores to subsequent loads whenever possible. The output is suitable as an input to heir-translate --emit-verilog. Retains affine.load and affine.store ops that cannot be removed (e.g., reading from the input and writing to the output, or loading from a memref with a variable index).

The pass pipeline assumes that the input is a valid TOSA MLIR model with stripped quantized types. The iree-import-tflite tool can lower a TFLite FlatBuffer to textual MLIR with --output-format=mlir-ir. See hello_world.tosa.mlir for an example.

--yosys-optimizer

Uses Yosys to booleanize and optimize MLIR functions.

This pass pipeline requires inputs to be in standard MLIR (arith, affine, func, memref). The pass imports the model to Yosys and runs passes to booleanize the circuit and then uses ABC to perform optimizations. We use standard LUT 3 cells. THe output of this pass includes arith constants and comb.truth_table ops.

The pass requires that the environment variable HEIR_ABC_BINARY contains the location of the ABC binary and that HEIR_YOSYS_SCRIPTS_DIR contains the location of the Yosys’ techlib files that are needed to execute the path.

This pass can be disabled by defining HEIR_NO_YOSYS; this will avoid Yosys library and ABC binary compilation, and avoid registration of this pass.

--tosa-to-boolean-tfhe

This is an experimental pipeline for end-to-end private inference.

Converts a TOSA MLIR model to tfhe_rust dialect defined by HEIR. It converts a tosa model to optimized boolean circuit using Yosys ABC optimizations. The resultant optimized boolean circuit in comb dialect is then converted to cggi and then to tfhe_rust exit dialect. This pipeline can be used with heir-translate –emit-tfhe-rust to generate code for tfhe-rs FHE library.

The pass requires that the environment variable HEIR_ABC_BINARY contains the location of the ABC binary and that HEIR_YOSYS_SCRIPTS_DIR contains the location of the Yosys’ techlib files that are needed to execute the path.

heir-translate

--emit-tfhe-rust

Code generation for the tfhe-rs FHE library. The library is based on the CGGI cryptosystem, and so this pass is most useful when paired with lowerings from the cggi dialect.

The version of tfhe-rs supported is defined in the end to end tfhe_rust tests.

Example input:

!sks = !tfhe_rust.server_key
!lut = !tfhe_rust.lookup_table
!eui3 = !tfhe_rust.eui3

func.func @test_apply_lookup_table(%sks : !sks, %lut: !lut, %input : !eui3) -> !eui3 {
  %v1 = tfhe_rust.apply_lookup_table %sks, %input, %lut : (!sks, !eui3, !lut) -> !eui3
  %v2 = tfhe_rust.add %sks, %input, %v1 : (!sks, !eui3, !eui3) -> !eui3
  %c1 = arith.constant 1 : i8
  %v3 = tfhe_rust.scalar_left_shift %sks, %v2, %c1 : (!sks, !eui3, i8) -> !eui3
  %v4 = tfhe_rust.apply_lookup_table %sks, %v3, %lut : (!sks, !eui3, !lut) -> !eui3
  return %v4 : !eui3
}

Example output:

use tfhe::shortint::prelude::*;

pub fn test_apply_lookup_table(
  v9: &ServerKey,
  v10: &LookupTableOwned,
  v11: &Ciphertext,
) -> Ciphertext {
  let v4 = v9.apply_lookup_table(&v11, &v10);
  let v5 = v9.unchecked_add(&v11, &v4);
  let v6 = 1;
  let v7 = v9.scalar_left_shift(&v5, v6);
  let v8 = v9.apply_lookup_table(&v7, &v10);
  v8
}

Note, the chosen variable names are arbitrary, and the resulting program still must be integrated with a larger Rust program.

--emit-verilog

Code generation for verilog from arith and memref. Expects a single top level func.func op as the entry point, which is converted to the output verilog module.

Example input:

module {
  func.func @main(%arg0: i8) -> (i8) {
    %c0 = arith.constant 0 : i32
    %c1 = arith.constant 1 : i32
    %c2 = arith.constant 2 : i32
    %c3 = arith.constant 3 : i32
    %0 = arith.extsi %arg0 : i8 to i32
    %1 = arith.subi %0, %c1 : i32
    %2 = arith.muli %1, %c2 : i32
    %3 = arith.addi %2, %c3 : i32
    %4 = arith.cmpi sge, %2, %c0 : i32
    %5 = arith.select %4, %c1, %c2 : i32
    %6 = arith.shrsi %3, %c1 : i32
    %7 = arith.shrui %3, %c1 : i32
    %out = arith.trunci %6 : i32 to i8
    return %out : i8
  }
}

Output:

module main(
  input wire signed [7:0] arg1,
  output wire signed [7:0] _out_
);
  wire signed [31:0] v2;
  wire signed [31:0] v3;
  wire signed [31:0] v4;
  wire signed [31:0] v5;
  wire signed [31:0] v6;
  wire signed [31:0] v7;
  wire signed [31:0] v8;
  wire signed [31:0] v9;
  wire v10;
  wire signed [31:0] v11;
  wire signed [31:0] v12;
  wire signed [31:0] v13;
  wire signed [7:0] v14;

  assign v2 = 0;
  assign v3 = 1;
  assign v4 = 2;
  assign v5 = 3;
  assign v6 = {{24{arg1[7]}}, arg1};
  assign v7 = v6 - v3;
  assign v8 = v7 * v4;
  assign v9 = v8 + v5;
  assign v10 = v8 >= v2;
  assign v11 = v10 ? v3 : v4;
  assign v12 = v9 >>> v3;
  assign v13 = v9 >> v3;
  assign v14 = v12[7:0];
  assign _out_ = v14;
endmodule

--emit-metadata

Prints a json object describing the function signatures. Used for code generation after --emit-verilog.

Example input:

module {
  func.func @main(%arg0: memref<80xi8>) -> memref<1x3x2x1xi8> {
    %alloc_0 = memref.alloc() {alignment = 64 : i64} : memref<1x3x2x1xi8>
    return %alloc_0 : memref<1x3x2x1xi8>
  }
}

Example output:

{
  "functions": [
    {
      "name": "main",
      "params": [
        {
          "index": 0,
          "type": {
            "memref": {
              "element_type": {
                "integer": {
                  "is_signed": false,
                  "width": 8
                }
              },
              "shape": [80]
            }
          }
        }
      ],
      "return_types": [{
        "memref": {
          "element_type": {
            "integer": {
              "is_signed": false,
              "width": 8
            }
          },
          "shape": [1, 3, 2, 1]
        }
      }]
    }
  ]
}

8 - Getting Started

Prerequisites

Clone and build the project

git clone git@github.com:google/heir.git && cd heir
bazel build @heir//tools:heir-opt

Some passes in this repository require Yosys as a dependency (--yosys-optimizer). If you would like to skip Yosys and ABC compilation to speed up builds, use the following build setting:

bazel build --//:enable_yosys=0 @heir//tools:heir-opt

Optional: Run the tests

bazel test @heir//...

Like above, run the following to skip tests that depend on Yosys:

bazel test --//:enable_yosys=0 --test_tag_filters=-yosys @heir//...

Run the dot-product example

The dot-product program computes the dot product of two length-8 vectors of 16-bit integers (i16 in MLIR parlance). This example will showcase the OpenFHE backend by manually calling the relevant compiler passes and setting up a C++ harness to call into the HEIR-generated functions.

Note: other backends are similar, but the different backends are in varying stages of development.

The input program is in tests/openfhe/end_to_end/dot_product_8.mlir. Support for standard input languages like C and C++ are currently experimental at best, but eventually we would use an MLIR-based tool to convert an input language to MLIR like in that file. The program is below:

func.func @dot_product(%arg0: tensor<8xi16>, %arg1: tensor<8xi16>) -> i16 {
  %c0 = arith.constant 0 : index
  %c0_si16 = arith.constant 0 : i16
  %0 = affine.for %arg2 = 0 to 8 iter_args(%iter = %c0_si16) -> (i16) {
    %1 = tensor.extract %arg0[%arg2] : tensor<8xi16>
    %2 = tensor.extract %arg1[%arg2] : tensor<8xi16>
    %3 = arith.muli %1, %2 : i16
    %4 = arith.addi %iter, %3 : i16
    affine.yield %4 : i16
  }
  return %0 : i16
}

For an introduction to MLIR syntax, see the official docs or this blog post.

Now we run the heir-opt command to optimize and compile the program.

bazel run //tools:heir-opt -- \
--mlir-to-openfhe-bgv='entry-function=dot_product ciphertext-degree=8' \
$PWD/tests/openfhe/end_to_end/dot_product_8.mlir > output.mlir

This produces a file in the openfhe exit dialect (part of HEIR). The raw output is rather verbose, and an abbreviated version is shown below.

!tensor_ct = !lwe.rlwe_ciphertext<..., underlying_type = tensor<8xi16>>
!scalar_ct = !lwe.rlwe_ciphertext<..., underlying_type = i16>
!mul_ct = !lwe.rlwe_ciphertext<..., underlying_type = tensor<8xi16>>
!tensor_plaintext = lwe.rlwe_plaintext<..., underlying_type = tensor<8xi16>>
module {
  func.func @dot_product(%arg0: !openfhe.crypto_context, %arg1: !tensor_ct, %arg2: !tensor_ct) -> !scalar_ct {
    %c1 = arith.constant 1 : index
    %c2 = arith.constant 2 : index
    %c4 = arith.constant 4 : index
    %c7 = arith.constant 7 : index
    %0 = openfhe.mul_no_relin %arg0, %arg1, %arg2 : (!openfhe.crypto_context, !tensor_ct, !tensor_ct) -> !mul_ct
    %1 = openfhe.relin %arg0, %0 : (!openfhe.crypto_context, !mul_ct) -> !tensor_ct
    %2 = arith.index_cast %c4 : index to i64
    %3 = openfhe.rot %arg0, %1, %2 : (!openfhe.crypto_context, !tensor_ct, i64) -> !tensor_ct
    %4 = openfhe.add %arg0, %1, %3 : (!openfhe.crypto_context, !tensor_ct, !tensor_ct) -> !tensor_ct
    %5 = arith.index_cast %c2 : index to i64
    %6 = openfhe.rot %arg0, %4, %5 : (!openfhe.crypto_context, !tensor_ct, i64) -> !tensor_ct
    %7 = openfhe.add %arg0, %4, %6 : (!openfhe.crypto_context, !tensor_ct, !tensor_ct) -> !tensor_ct
    %8 = arith.index_cast %c1 : index to i64
    %9 = openfhe.rot %arg0, %7, %8 : (!openfhe.crypto_context, !tensor_ct, i64) -> !tensor_ct
    %10 = openfhe.add %arg0, %7, %9 : (!openfhe.crypto_context, !tensor_ct, !tensor_ct) -> !tensor_ct
    %cst = arith.constant dense<[0, 0, 0, 0, 0, 0, 0, 1]> : tensor<8xi16>
    %11 = lwe.rlwe_encode %cst {encoding = #lwe.polynomial_evaluation_encoding<cleartext_start = 16, cleartext_bitwidth = 16>, ring = #_polynomial.ring<cmod=463187969, ideal=#_polynomial.polynomial<1 + x**8>>} : tensor<8xi16> -> !tensor_plaintext
    %12 = openfhe.mul_plain %arg0, %10, %11 : (!openfhe.crypto_context, !tensor_ct, !tensor_plaintext) -> !tensor_ct
    %13 = arith.index_cast %c7 : index to i64
    %14 = openfhe.rot %arg0, %12, %13 : (!openfhe.crypto_context, !tensor_ct, i64) -> !tensor_ct
    %15 = lwe.reinterpret_underlying_type %14 : !tensor_ct to !scalar_ct
    return %15 : !scalar_ct
  }
  func.func @dot_product__encrypt__arg0(%arg0: !openfhe.crypto_context, %arg1: tensor<8xi16>, %arg2: !openfhe.public_key) -> !tensor_ct
    ...
  }
  func.func @dot_product__encrypt__arg1(%arg0: !openfhe.crypto_context, %arg1: tensor<8xi16>, %arg2: !openfhe.public_key) -> !tensor_ct
    ...
  }
  func.func @dot_product__decrypt__result0(%arg0: !openfhe.crypto_context, %arg1: !scalar_ct, %arg2: !openfhe.private_key) -> i16 {
    ...
  }
}

Next, we use the heir-translate tool to run code generation for the OpenFHE pke API.

bazel run //tools:heir-translate -- --emit-openfhe-pke-header $PWD/output.mlir > heir_output.h
bazel run //tools:heir-translate -- --emit-openfhe-pke $PWD/output.mlir > heir_output.cpp

The results:

// heir_output.h
#include "src/pke/include/openfhe.h" // from @openfhe

using namespace lbcrypto;
using CiphertextT = ConstCiphertext<DCRTPoly>;
using CryptoContextT = CryptoContext<DCRTPoly>;
using EvalKeyT = EvalKey<DCRTPoly>;
using PlaintextT = Plaintext;
using PrivateKeyT = PrivateKey<DCRTPoly>;
using PublicKeyT = PublicKey<DCRTPoly>;

CiphertextT dot_product(CryptoContextT v0, CiphertextT v1, CiphertextT v2);
CiphertextT dot_product__encrypt__arg0(CryptoContextT v24, std::vector<int16_t> v25, PublicKeyT v26);
CiphertextT dot_product__encrypt__arg1(CryptoContextT v29, std::vector<int16_t> v30, PublicKeyT v31);
int16_t dot_product__decrypt__result0(CryptoContextT v34, CiphertextT v35, PrivateKeyT v36);

// heir_output.cpp
#include "src/pke/include/openfhe.h" // from @openfhe

using namespace lbcrypto;
using CiphertextT = ConstCiphertext<DCRTPoly>;
using CryptoContextT = CryptoContext<DCRTPoly>;
using EvalKeyT = EvalKey<DCRTPoly>;
using PlaintextT = Plaintext;
using PrivateKeyT = PrivateKey<DCRTPoly>;
using PublicKeyT = PublicKey<DCRTPoly>;

CiphertextT dot_product(CryptoContextT v0, CiphertextT v1, CiphertextT v2) {
  size_t v3 = 1;
  size_t v4 = 2;
  size_t v5 = 4;
  size_t v6 = 7;
  const auto& v7 = v0->EvalMultNoRelin(v1, v2);
  const auto& v8 = v0->Relinearize(v7);
  int64_t v9 = static_cast<int64_t>(v5);
  const auto& v10 = v0->EvalRotate(v8, v9);
  const auto& v11 = v0->EvalAdd(v8, v10);
  int64_t v12 = static_cast<int64_t>(v4);
  const auto& v13 = v0->EvalRotate(v11, v12);
  const auto& v14 = v0->EvalAdd(v11, v13);
  int64_t v15 = static_cast<int64_t>(v3);
  const auto& v16 = v0->EvalRotate(v14, v15);
  const auto& v17 = v0->EvalAdd(v14, v16);
  std::vector<int16_t> v18 = {0, 0, 0, 0, 0, 0, 0, 1};
  std::vector<int64_t> v18_cast(std::begin(v18), std::end(v18));
  const auto& v19 = v0->MakePackedPlaintext(v18_cast);
  const auto& v20 = v0->EvalMult(v17, v19);
  int64_t v21 = static_cast<int64_t>(v6);
  const auto& v22 = v0->EvalRotate(v20, v21);
  const auto& v23 = v22;
  return v23;
}
CiphertextT dot_product__encrypt__arg0(CryptoContextT v24, std::vector<int16_t> v25, PublicKeyT v26) {
  ...
}
CiphertextT dot_product__encrypt__arg1(CryptoContextT v29, std::vector<int16_t> v30, PublicKeyT v31) {
  ...
}
int16_t dot_product__decrypt__result0(CryptoContextT v34, CiphertextT v35, PrivateKeyT v36) {
  ...
}

At this point we can compile the program as we would a normal OpenFHE program. Note that the above two files just contain the compiled function and encryption/decryption helpers, and does not include any code that provides specific inputs or calls these functions.

Next we’ll create a harness that provides sample inputs, encrypts them, runs the compiled function, and decrypts the result. Once you have the generated header and cpp files, you can do this with any build system. We will use bazel for consistency.

Create a file called BUILD in the same directory as the header and cpp files above, with the following contents:

# A library build target that encapsulates the HEIR-generated code.
cc_library(
    name = "dot_product_codegen",
    srcs = ["heir_output.cpp"],
    hdrs = ["heir_output.h"],
    deps = ["@openfhe//:pke"],
)

# An executable build target that contains your main function and links
# against the above.
cc_binary(
    name = "dot_product_main",
    srcs = ["dot_product_main.cpp"],
    deps = [
        ":dot_product_codegen",
        "@openfhe//:pke",
        "@openfhe//:core",
    ],
)

Where dot_product_main.cpp is a new file containing

#include <cstdint>
#include <vector>

#include "src/pke/include/openfhe.h" // from @openfhe
#include "heir_output.h"

int main(int argc, char *argv[]) {
  CCParams<CryptoContextBGVRNS> parameters;
  // TODO(#661): replace this setup with a HEIR-generated helper function
  parameters.SetMultiplicativeDepth(2);
  parameters.SetPlaintextModulus(65537);
  CryptoContext<DCRTPoly> cryptoContext = GenCryptoContext(parameters);
  cryptoContext->Enable(PKE);
  cryptoContext->Enable(KEYSWITCH);
  cryptoContext->Enable(LEVELEDSHE);

  KeyPair<DCRTPoly> keyPair;
  keyPair = cryptoContext->KeyGen();
  cryptoContext->EvalMultKeyGen(keyPair.secretKey);
  cryptoContext->EvalRotateKeyGen(keyPair.secretKey, {1, 2, 4, 7});

  int32_t n = cryptoContext->GetCryptoParameters()
                  ->GetElementParams()
                  ->GetCyclotomicOrder() /
              2;
  int16_t arg0Vals[8] = {1, 2, 3, 4, 5, 6, 7, 8};
  int16_t arg1Vals[8] = {2, 3, 4, 5, 6, 7, 8, 9};
  int64_t expected = 240;

  std::vector<int16_t> arg0;
  std::vector<int16_t> arg1;
  arg0.reserve(n);
  arg1.reserve(n);

  // TODO(#645): support cyclic repetition in add-client-interface
  for (int i = 0; i < n; ++i) {
    arg0.push_back(arg0Vals[i % 8]);
    arg1.push_back(arg1Vals[i % 8]);
  }

  auto arg0Encrypted =
      dot_product__encrypt__arg0(cryptoContext, arg0, keyPair.publicKey);
  auto arg1Encrypted =
      dot_product__encrypt__arg1(cryptoContext, arg1, keyPair.publicKey);
  auto outputEncrypted =
      dot_product(cryptoContext, arg0Encrypted, arg1Encrypted);
  auto actual = dot_product__decrypt__result0(cryptoContext, outputEncrypted,
                                              keyPair.secretKey);

  std::cout << "Expected: " << expected << "\n";
  std::cout << "Actual: " << actual << "\n";

  return 0;
}

Then run and show the results:

$ bazel run dot_product_main
Expected: 240
Actual: 240

Optional: Run a custom heir-opt pipeline

HEIR comes with two central binaries, heir-opt for running optimization passes and dialect conversions, and heir-translate for backend code generation. To see the list of available passes in each one, run the binary with --help:

bazel run //tools:heir-opt -- --help
bazel run //tools:heir-translate -- --help

Once you’ve chosen a pass or --pass-pipeline to run, execute it on the desired file. For example, you can run a test file through heir-opt to see its output. Note that when the binary is run via bazel, you must pass absolute paths to input files. You can also access the underlying binary at bazel-bin/tools/heir-opt, provided it has already been built.

bazel run //tools:heir-opt -- \
  --comb-to-cggi -cse \
  $PWD/tests/comb_to_cggi/add_one.mlir

To convert an existing lit test to a bazel run command for manual tweaking and introspection (e.g., adding --debug or --mlir-print-ir-after-all to see how he IR changes with each pass), use python scripts/lit_to_bazel.py.

# after pip installing requirements-dev.txt
python scripts/lit_to_bazel.py tests/simd/box_blur_64x64.mlir

Which outputs

bazel run --noallow_analysis_cache_discard //tools:heir-opt -- \
--secretize=entry-function=box_blur --wrap-generic --canonicalize --cse --full-loop-unroll \
--insert-rotate --cse --canonicalize --collapse-insertion-chains \
--canonicalize --cse /path/to/heir/tests/simd/box_blur_64x64.mlir

Using a pre-built nightly binary

HEIR releases a nightly binary for Linux x86-64. This is intended for testing compiler passes and not for production use.

wget https://github.com/google/heir/releases/download/nightly/heir-opt
chmod +x heir-opt
./heir-opt --help

Then you can run the examples above, replacing bazel run //tools:heir-opt -- with ./heir-opt. HEIR also publishes heir-translate and heir-lsp in the same way.

Developing in HEIR

We use pre-commit to manage a series of git pre-commit hooks for the project; for example, each time you commit code, the hooks will make sure that your C++ is formatted properly. If your code isn’t, the hook will format it, so when you try to commit the second time you’ll get past the hook.

All hooks are defined in .pre-commit-config.yaml. To install these hooks, run

pip install -r requirements-dev.txt

Then install the hooks to run automatically on git commit:

pre-commit install

To run them manually, run

pre-commit run --all-files

Creating a New Pass

The scripts/templates folder contains Python scripts to create boilerplate for new conversion or (dialect-specific) transform passes. These should be used when the tablegen files containing existing pass definitions in the expected filepaths are not already present. Otherwise, you should modify the existing tablegen files directly.

Conversion Pass

To create a new conversion pass, run a command similar to the following:

python scripts/templates/templates.py new_conversion_pass \
--source_dialect_name=CGGI \
--source_dialect_namespace=cggi \
--source_dialect_mnemonic=cggi \
--target_dialect_name=TfheRust \
--target_dialect_namespace=tfhe_rust \
--target_dialect_mnemonic=tfhe_rust

In order to build the resulting code, you must fix the labeled FIXMEs in the type converter and the op conversion patterns.

Transform Passes

To create a transform or rewrite pass that operates on a dialect, run a command similar to the following:

python scripts/templates/templates.py new_dialect_transform \
--pass_name=ForgetSecrets \
--pass_flag=forget-secrets \
--dialect_name=Secret \
--dialect_namespace=secret \
--force=false

If the transform does not operate from and to a specific dialect, use

python scripts/templates/templates.py new_transform \
--pass_name=ForgetSecrets \
--pass_flag=forget-secrets \
--force=false