diff --git a/changelogs/unreleased/fix__deferred-template-expressions.yaml b/changelogs/unreleased/fix__deferred-template-expressions.yaml new file mode 100644 index 0000000000..7f00f94cd6 --- /dev/null +++ b/changelogs/unreleased/fix__deferred-template-expressions.yaml @@ -0,0 +1,5 @@ +fixed: + - Evaluate target-used template expressions when their value and type dependencies become concrete, preserving normalized expressions across partial specialization + - Preserve ordered explicit arguments on rebuilt nested calls and materialize enclosing bindings inside array, struct, POD, and scalar type arguments + - Canonicalize equivalent felt spellings before function and struct specialization identity is computed + - Roll back failed prospective specializations without retaining generated definitions or symbol-table entries, and suppress warnings from discarded clones diff --git a/changelogs/unreleased/fix__felt-materialization-verification.yaml b/changelogs/unreleased/fix__felt-materialization-verification.yaml new file mode 100644 index 0000000000..5d033fa23e --- /dev/null +++ b/changelogs/unreleased/fix__felt-materialization-verification.yaml @@ -0,0 +1,2 @@ +fixed: + - Materialize fieldless felt template constants in the required field and reject known field or value conflicts in concrete and symbolic arguments diff --git a/changelogs/unreleased/fix__function-specialization-reuse.yaml b/changelogs/unreleased/fix__function-specialization-reuse.yaml new file mode 100644 index 0000000000..483b98ee33 --- /dev/null +++ b/changelogs/unreleased/fix__function-specialization-reuse.yaml @@ -0,0 +1,2 @@ +fixed: + - Reuse fully instantiated free-function clones only when the source definition and ordered concrete parameter bindings match diff --git a/doc/doxygen/03_syntax.md b/doc/doxygen/03_syntax.md index 47f9d349f1..d2b6e860b2 100644 --- a/doc/doxygen/03_syntax.md +++ b/doc/doxygen/03_syntax.md @@ -34,7 +34,7 @@ module attributes {llzk.lang = "circom"} { - `index`: (MLIR builtin) Machine integer. - `felt.type`: Finite field element. - `array.type`: Aggregate type with indexed [pseudo-homogeneous](\ref pseudo-homogeneous) elements. Element type cannot be another array type, instead multi-dimensional arrays are specified with a comma-separated list of dimension sizes. Each dimension size can be specified as an integer literal, a symbol (referring to a template parameter within a templated `struct.def`), or an [affine_map](https://mlir.llvm.org/docs/Dialects/Affine/#polyhedral-structures) (used when creating arrays within a loop where the dimension size depends on the loop iteration variable). -- `struct.type<[..]>`: Aggregate type with named heterogeneous elements corresponding to a `struct.def`. Generally correlates to components/functions in the source language. Constituent elements may be local variables, subcomponents, and/or called functions. Optionally includes a list of parameters to instantiate a templated `struct.def` where each parameter can be an integer literal, a symbol (referring to a template parameter within a templated `struct.def`), a type used to instantiate a `poly.tvar<@N>` (see below), or an [affine_map](https://mlir.llvm.org/docs/Dialects/Affine/#polyhedral-structures) (used when the parameter of a templated `struct.type` depends on a loop iteration variable). +- `struct.type<@Name<[...]>>`: Aggregate type whose named heterogeneous elements are declared by a `struct.def`. For a definition nested in a `poly.template`, an optional instantiation list supplies one argument per `poly.param` in declaration order. An argument can be an integer literal, a felt constant such as `#felt`, a symbol that resolves to an enclosing template parameter or global constant, a type used to instantiate a `poly.tvar<@N>` (see below), or an [affine_map](https://mlir.llvm.org/docs/Dialects/Affine/#polyhedral-structures) used when the argument depends on a loop iteration variable. - `pod.type<..>`: Plain Old Data aggregate type with named heterogeneous elements. Unlike `struct.type`, there is no associated named declaration, the type itself specifies all constituent element types. It can be used more freely than `struct.type` since it has fewer restrictions on modifications. - `poly.tvar<@N>`: Placeholder type variable within a templated `struct.def` that may be instantiated with different types. - `string.type`: Sequence of characters. @@ -46,6 +46,7 @@ LLZK supports arrays where the element type is not truly homogeneous, specifical ## Semantic Rules - Each `array.new` operation creates a fresh mutable array allocation. Two identical `array.new` operations are not interchangeable when either result may be read or written. The same is true for `pod.new`. +- Felt arguments can instantiate parameters on `struct.type`, templated free-function calls, and `verif.include`. A fieldless felt restriction accepts any felt field. For a fielded restriction, a fieldless felt constant or integer is materialized in the required field, while an explicitly fielded constant or typed symbol must name that same field; an untyped symbol cannot establish a required field. If a call or inclusion signature independently infers the same parameter, all known fields and concrete values must also agree. - A `function.def` argument may have `function.arg_name = "..."` to preserve the source-level argument name independently from the SSA name printed by MLIR. The value must be a non-empty, untyped string attribute; typed string attributes such as `"x" : i1` are rejected. Attached argument names must be unique within the function. Argument-splitting transforms derive names for generated arguments, such as `input[0]` for array elements or `self.member` for struct members. - Ops marked with the `WitnessGen` trait can only be used in functions with the `allow_witness` attribute (`compute()` within `struct.def` has this by default). Similarly, ops marked with the `ConstraintGen` trait can only be used in functions with the `allow_constraint` attribute (`constrain()` within `struct.def` has this by default). - Functions with the `allow_witness` attribute can only call other functions marked with `allow_witness`. Likewise for `allow_constraint`. diff --git a/include/llzk/Dialect/Felt/IR/Attrs.td b/include/llzk/Dialect/Felt/IR/Attrs.td index 8853130a3e..aff5b851c7 100644 --- a/include/llzk/Dialect/Felt/IR/Attrs.td +++ b/include/llzk/Dialect/Felt/IR/Attrs.td @@ -72,6 +72,11 @@ def LLZK_FeltConstAttr /// Returns the field name from the stored FeltType, or a null StringAttr /// if no type is stored. ::mlir::StringAttr getFieldName() const; + + /// Return a new constant with a compatible materialized type; this attribute is not mutated. + /// A fieldless expected type preserves an explicit field, while a fielded expected type is + /// applied to a fieldless constant. Materialization fails only when both fields differ. + ::mlir::FailureOr materializeAs(FeltType expectedType) const; }]; } diff --git a/include/llzk/Dialect/Function/IR/Ops.td b/include/llzk/Dialect/Function/IR/Ops.td index 51f4d05f4c..452b1a1153 100644 --- a/include/llzk/Dialect/Function/IR/Ops.td +++ b/include/llzk/Dialect/Function/IR/Ops.td @@ -332,7 +332,7 @@ def CallOp : FunctionDialectOp< When the return StructType of a `compute()` function uses AffineMapAttr to express struct parameter(s) that depend on a loop variable, the optional - instantiation parameter list of this operation must be used to instatiate + instantiation parameter list of this operation must be used to instantiate all AffineMap used as parameters to the StructType. Examples: @@ -341,14 +341,16 @@ def CallOp : FunctionDialectOp< %r = function.call @A::@compute(%x){(%i)} : (!felt.type) -> !struct.type<@A<[#M]>> ``` - When the call targets a free function within a `poly.template` region, the optional - template parameter list can be used to instantiate all `poly.param` symbols within - the template. If all `poly.param` symbols are used within the function signature, - this can be elided. Otherwise, it is required to instantiate the function. The `?` - wildcard can be used for any `poly.param` with a `poly.tvar` type restriction, even - those that cannot be inferred from the function signature. The wildcard allows for - inference of the type within the function body itself during the flattening pass - but may fail if the type cannot be inferred from the function body. + When the call targets a free function within a `poly.template`, the optional template + parameter list supplies one argument per `poly.param`, in declaration order. The list may be + omitted only when every parameter appears in the function signature; omitted values are + inferred by unifying the call's operand and result types with that signature. The `?` wildcard + is valid only for a parameter with a `poly.tvar` restriction and leaves that type for a later + inference transformation. + + Every explicit or inferred argument must satisfy its `poly.param` type restriction. If the + function signature independently infers a value for an explicitly supplied parameter, all + known fields and concrete values must agree. }]; // See `VerifySizesForMultiAffineOps` for more explanation of these arguments. @@ -460,7 +462,7 @@ def CallOp : FunctionDialectOp< /// or else IR verification will fail. ::mlir::FunctionType getTypeSignature(); - /// Attempt type unfication between the inferred FunctionType from this CallOp (as LHS) and + /// Attempt type unification between the inferred FunctionType from this CallOp (as LHS) and /// the given FunctionType (as RHS). If successful, return a UnificationMap containing the /// unifications that were made. Otherwise, return failure. ::mlir::FailureOr unifyTypeSignature(::mlir::FunctionType other); @@ -539,8 +541,10 @@ def CallOp : FunctionDialectOp< /// `UnificationMap` is expected to contain the unification results of this `CallOp` against /// the target function type signature. /// - /// Pre-condition assertions: - /// - `!isNullOrEmpty(getTemplateParamsAttr())` + /// The template parameter list may be omitted when all parameters are inferable from the + /// function type signature. + /// + /// Pre-condition assertions when the list is present: /// - `getTemplateParamsAttr().size() == llvm::range_size(targetParamDefs)` ::mlir::LogicalResult verifyTemplateParamsMatchInferred( ::llvm::iterator_range<::mlir::Region::op_iterator<::llzk::polymorphic::TemplateParamOp>> targetParamDefs, diff --git a/include/llzk/Dialect/Polymorphic/IR/Ops.td b/include/llzk/Dialect/Polymorphic/IR/Ops.td index bf504be368..2e93639888 100644 --- a/include/llzk/Dialect/Polymorphic/IR/Ops.td +++ b/include/llzk/Dialect/Polymorphic/IR/Ops.td @@ -264,18 +264,20 @@ def LLZK_UnifiableCastOp : PolymorphicDialectOp<"unifiable_cast", [Pure]> { This operation reinterprets a value as a different type with the restriction that the input and output types of the cast are unifiable. - Most ops that accept LLZK types accept unifiable types as input and thus there - is no need for casting between types. This op is meant to be used in situations where - is not possible to modify the given or the target type and they are different but unifiable. - For example, inside a conversion pattern the driver may introduce `unrealized_conversion_cast` - operations if the types are not equal. This will happen regardless of whether the two types unify. - This cast can be introduced instead of the default cast operation to satisfy MLIR's assumptions - on type equality. + Most operations that accept LLZK types also accept operands whose types unify, so an explicit + cast is normally unnecessary. Use this operation when neither the input nor result type can be + changed but MLIR still requires exact type equality. For example, a conversion pattern can use + this operation instead of an `unrealized_conversion_cast` while preserving the requirement that + the unequal types unify. + + During constant folding, this operation returns its input constant only when the input and + result types are identical. Non-constant inputs do not fold. Merely unifiable but unequal + types do not fold because the input has a different type from the result. Example: ```llzk %0 = some_other_op : !array.type<@N x !felt.type> - %1 = unifiable_cast %0 : (!array.type<@N x @felt.type>) -> !array.type (s0 + s1)> x !felt.type> + %1 = poly.unifiable_cast %0 : (!array.type<@N x !felt.type>) -> !array.type (s0 + s1)> x !felt.type> ``` }]; @@ -286,6 +288,7 @@ def LLZK_UnifiableCastOp : PolymorphicDialectOp<"unifiable_cast", [Pure]> { }]; let hasVerifier = 1; + let hasFolder = 1; } def LLZK_ApplyMapOp : PolymorphicDialectOp<"applymap", [Pure]> { diff --git a/include/llzk/Dialect/Polymorphic/Transforms/TransformationPasses.td b/include/llzk/Dialect/Polymorphic/Transforms/TransformationPasses.td index d4dd70eb6e..cf0ccd440d 100644 --- a/include/llzk/Dialect/Polymorphic/Transforms/TransformationPasses.td +++ b/include/llzk/Dialect/Polymorphic/Transforms/TransformationPasses.td @@ -53,15 +53,31 @@ def EmptyTemplateRemovalPass : LLZKPass<"llzk-drop-empty-templates"> { } def FlatteningPass : LLZKPass<"llzk-flatten"> { - let summary = "Flatten structs and unroll loops"; + let summary = + "Instantiate templates, unroll loops, and propagate instantiated types"; let description = [{ - Performs the following transformations: - - Instantiate `affine_map` parameters of StructType and ArrayType - to constant values using the arguments at the instantiation site - - Replace parameterized structs with flattened (i.e., no parameter) - versions of those structs based on requested return type at calls - to `compute()` functions and unroll loops - - Unroll loops + Iterates the following transformations to a fixpoint, bounded by `max-iter`: + + - Instantiate parameterized structs reached from `llzk.main` or struct uses. Specialize free + functions from concrete explicit arguments, signature unification, or body-inferred wildcard + types. If only some parameters are concrete, keep the specialization in a reduced + `poly.template`. + - Evaluate target-used `poly.expr` values whose dependencies are concrete. A partial + specialization retains normalized copies of target-used expressions that still depend on + remaining parameters. A concrete expression that cannot fold, or an expression that remains + deferred in a full specialization, is an error. + - Apply concrete bindings to scalar type variables, array dimensions and element types, + parameterized struct arguments, POD record types, and explicit arguments on nested calls. + - Unroll loops with known iteration bounds, instantiate `affine_map` parameters of struct and + array types, and propagate the resulting member, array, call, and function types. + + Warnings accumulated while converting a prospective struct are retained only after body + conversion succeeds. For a prospective free function, body conversion and nested-call + verification must both succeed. A failed prospective specialization and its accumulated + warnings are discarded. + + After the fixpoint, the pass applies the selected cleanup mode, removes empty templates, and + removes unused discardable array allocations. }]; // Implementation note: These options should be kept in sync with // `StructInliningFlatteningOptions` in `LLZKTransformationPassPipelines.h`. diff --git a/include/llzk/Dialect/Shared/TypeConversionPatterns.h b/include/llzk/Dialect/Shared/TypeConversionPatterns.h index 2527661413..49eb1d220b 100644 --- a/include/llzk/Dialect/Shared/TypeConversionPatterns.h +++ b/include/llzk/Dialect/Shared/TypeConversionPatterns.h @@ -206,7 +206,8 @@ class CreateArrayOpClassReplacePattern : public mlir::OpConversionPattern { public: CallOpClassReplacePattern(mlir::TypeConverter &converter, mlir::MLIRContext *ctx) @@ -219,9 +220,12 @@ class CallOpClassReplacePattern : public mlir::OpConversionPatternconvertTypes(op.getResultTypes(), newResultTypes))) { return op->emitError("Could not convert Op result types."); } + mlir::ArrayAttr templateParamsAttr = op.getTemplateParamsAttr(); + llvm::ArrayRef templateParams = + templateParamsAttr ? templateParamsAttr.getValue() : llvm::ArrayRef(); replaceOpWithNewOp( rewriter, op, newResultTypes, op.getCalleeAttr(), adapter.getMapOperands(), - op.getNumDimsPerMapAttr(), adapter.getArgOperands() + op.getNumDimsPerMapAttr(), adapter.getArgOperands(), templateParams ); return mlir::success(); } diff --git a/include/llzk/Dialect/Struct/IR/Types.td b/include/llzk/Dialect/Struct/IR/Types.td index cd8a1966c1..586837ba2e 100644 --- a/include/llzk/Dialect/Struct/IR/Types.td +++ b/include/llzk/Dialect/Struct/IR/Types.td @@ -27,22 +27,25 @@ class StructDialectType { let summary = "circuit component"; let description = [{ - Type of a `struct` op instance. For structs that contain template parameters, - the type must contain a list of attributes that instantiate the template - parameters, one per parameter. Each attribute must be one of the following: + Type of a `struct.def` instance. An instantiation argument list supplies one attribute per + `poly.param` in the definition's enclosing `poly.template`, in declaration order. Every + argument must satisfy the corresponding parameter's type restriction; a symbol argument is + checked using its resolved enclosing template binding or global definition. Each attribute + must be one of the following: - IntegerAttr (with IndexType), specifying a fixed parameter value - - SymbolRefAttr, specifying a parameter value defined by a struct parameter + - FeltConstAttr, specifying a fixed finite-field parameter value + - SymbolRefAttr, specifying a parameter value defined by an enclosing template parameter or global constant - - AffineMapAttr, for an array of struct elements whose template parameters - vary based on some fixed pattern. - - TypeAttr, for specifying a type parameter. + - AffineMapAttr, specifying a single-result affine expression for an argument that varies + across an array of struct elements + - TypeAttr, specifying a type parameter ```llzk // Type for struct `A` with no parameters. !struct.type<@A> - // Type for struct `B` with IntegerAttr and SymbolRefAttr parameters. - !struct.type<@B<[5, @C]>> + // Type for struct `B` with IntegerAttr, FeltConstAttr, and SymbolRefAttr parameters. + !struct.type<@B<[5, #felt, @C]>> // Type for struct `C` with TypeAttr and IntegerAttr parameters. !struct.type<@C<[!felt.type, 24]>> @@ -99,7 +102,7 @@ def LLZK_StructType : StructDialectType<"Struct", "type"> { // Verifies that this type references a valid struct, relative to the given `op`. ::mlir::LogicalResult verifySymbolRef(::mlir::SymbolTableCollection &symbolTable, ::mlir::Operation *op); - /// Returns wether the struct this type refers to has members marked as columns. + /// Returns whether the struct this type refers to has members marked as columns. /// A lookup is necessary first and will forward the failure state if it fails. mlir::LogicalResult hasColumns(mlir::SymbolTableCollection &symbolTable, mlir::Operation *op) const; }]; diff --git a/include/llzk/Dialect/Verif/IR/Ops.td b/include/llzk/Dialect/Verif/IR/Ops.td index ba4de42cd4..e6a084a0da 100644 --- a/include/llzk/Dialect/Verif/IR/Ops.td +++ b/include/llzk/Dialect/Verif/IR/Ops.td @@ -367,6 +367,14 @@ def IncludeOp let description = [{ Invokes another specification contract from another contract, effectively including the specifications from another specification into the current contract. + + When the target contract is inside a `poly.template`, the optional template parameter list + supplies one argument per `poly.param`, in declaration order. The list may be omitted when + every parameter appears in the contract signature; omitted values are derived from the + inclusion operand types. Every explicit or inferred argument must satisfy its declared type + restriction. If both forms provide a value for the same parameter, all known fields and + concrete values must agree. The `?` wildcard is valid only for a parameter with a `poly.tvar` + restriction and defers that type to a later inference transformation. }]; // See `VerifySizesForMultiAffineOps` for more explanation of these arguments. @@ -460,14 +468,14 @@ def IncludeOp /// Required by CallOpInterface ::mlir::Operation *resolveCallable(); - /// Return the FunctionType inferred from the arg operands of this CallOp. - /// This is not necessarily the same as the callee's FunctionType but should unify with it - /// or else IR verification will fail. + /// Return the FunctionType inferred from the argument operands of this IncludeOp. + /// This is not necessarily the same as the target contract's FunctionType but must unify with + /// it or else IR verification will fail. ::mlir::FunctionType getTypeSignature(); - /// Attempt type unfication between the inferred FunctionType from this CallOp (as LHS) and - /// the given FunctionType (as RHS). If successful, return a UnificationMap containing the - /// unifications that were made. Otherwise, return failure. + /// Attempt type unification between the inferred FunctionType from this IncludeOp (as LHS) and + /// the given contract FunctionType (as RHS). If successful, return a UnificationMap containing + /// the unifications that were made. Otherwise, return failure. ::mlir::FailureOr unifyTypeSignature(::mlir::FunctionType other); /// Return `true` iff the contract targets a struct type. @@ -477,7 +485,7 @@ def IncludeOp /// assuming the target of the contract is a struct target. ::mlir::Value getSelfValue(); - /// Resolve and return the target Contract for this CallOp. + /// Resolve and return the target Contract for this IncludeOp. ::mlir::FailureOr<::llzk::SymbolLookupResult<::llzk::verif::ContractOp>> getCalleeTarget(::mlir::SymbolTableCollection &tables); @@ -485,14 +493,14 @@ def IncludeOp /// so it can be passed to the builders as an `ArrayRef`. static ::llvm::SmallVector<::mlir::ValueRange> toVectorOfValueRange(::mlir::OperandRangeRange); - /// Check type compatibility of the given template parameter value from this `CallOp` against + /// Check type compatibility of the given template parameter value from this `IncludeOp` against /// the declared type on the given `TemplateParamOp` (if any). ::mlir::LogicalResult verifyTemplateParamCompatibility( - ::mlir::Attribute paramFromCallOp, ::llzk::polymorphic::TemplateParamOp targetParam + ::mlir::Attribute paramFromIncludeOp, ::llzk::polymorphic::TemplateParamOp targetParam ); - /// Check type compatibility of each template parameter value provided in this `CallOp` against - /// the declared type on each `TemplateParamOp` (if any). + /// Check type compatibility of each template parameter value provided in this `IncludeOp` + /// against the declared type on each `TemplateParamOp` (if any). /// /// Pre-condition assertions: /// - `!isNullOrEmpty(getTemplateParamsAttr())` @@ -501,13 +509,15 @@ def IncludeOp ::llvm::iterator_range<::mlir::Region::op_iterator<::llzk::polymorphic::TemplateParamOp>> targetParamDefs ); - /// Verify that each template parameter value provided in this `CallOp` is consistent with + /// Verify that each template parameter value provided in this `IncludeOp` is consistent with /// the value inferred for the target `TemplateParamOp` in the given `UnificationMap`. The - /// `UnificationMap` is expected to contain the unification results of this `CallOp` against - /// the target function type signature. + /// `UnificationMap` is expected to contain the unification results of this `IncludeOp` against + /// the target contract type signature. /// - /// Pre-condition assertions: - /// - `!isNullOrEmpty(getTemplateParamsAttr())` + /// The template parameter list may be omitted when all parameters are inferable from the + /// contract type signature. + /// + /// Pre-condition assertions when the list is present: /// - `getTemplateParamsAttr().size() == llvm::range_size(targetParamDefs)` ::mlir::LogicalResult verifyTemplateParamsMatchInferred( ::llvm::iterator_range<::mlir::Region::op_iterator<::llzk::polymorphic::TemplateParamOp>> targetParamDefs, diff --git a/include/llzk/Util/SymbolHelper.h b/include/llzk/Util/SymbolHelper.h index d502d472f9..5eef044947 100644 --- a/include/llzk/Util/SymbolHelper.h +++ b/include/llzk/Util/SymbolHelper.h @@ -211,7 +211,7 @@ mlir::FailureOr getConstResolutionTemplate(mlir::SymbolTableCollection &tables, mlir::Operation *origin); /// Ensure that the given symbol (that is used as a parameter of the given type) can be resolved. -/// If `requiredParamType` is provided, any resolved template symbol must have exactly that type. +/// If `requiredParamType` is provided, any resolved template symbol must satisfy that restriction. mlir::LogicalResult verifyParamOfType( mlir::SymbolTableCollection &tables, mlir::SymbolRefAttr param, mlir::Type structOrArrayType, mlir::Operation *origin, std::optional requiredParamType = std::nullopt @@ -219,13 +219,24 @@ mlir::LogicalResult verifyParamOfType( /// Ensure that any symbols that appear within the given attributes (that are parameters of the /// given type) can be resolved. If `requiredParamType` is provided, any resolved template symbols -/// must have exactly that type. +/// must satisfy that restriction. mlir::LogicalResult verifyParamsOfType( mlir::SymbolTableCollection &tables, mlir::ArrayRef tyParams, mlir::Type structOrArrayType, mlir::Operation *origin, std::optional requiredParamType = std::nullopt ); +/// Compare explicit and signature-inferred template values. For a felt restriction, local template +/// bindings contribute type evidence and qualified globals contribute type and concrete-value +/// evidence. Return `false` for a known field or value conflict, and preserve the context-free +/// unifier's result when either symbol has no resolvable evidence. Return failure only when the +/// enclosing template scope itself cannot be resolved. Non-felt restrictions always use the +/// context-free unifier. +mlir::FailureOr resolvedTemplateParamValuesUnify( + mlir::SymbolTableCollection &tables, mlir::Operation *origin, mlir::Attribute explicitValue, + mlir::Attribute inferredValue, std::optional requiredParamType +); + /// Ensure that all symbols used within the type can be resolved. mlir::FailureOr verifyStructTypeResolution( mlir::SymbolTableCollection &tables, component::StructType ty, mlir::Operation *origin diff --git a/include/llzk/Util/TypeHelper.h b/include/llzk/Util/TypeHelper.h index bca953499f..fcebde2dcc 100644 --- a/include/llzk/Util/TypeHelper.h +++ b/include/llzk/Util/TypeHelper.h @@ -19,6 +19,8 @@ #include #include +#include + namespace llzk { // Forward declarations @@ -265,6 +267,41 @@ bool typesUnify( UnificationMap *unifications = nullptr ); +/// Return `true` iff an actual template argument type satisfies a required type restriction. +/// Unlike `typesUnify`, this check is directional: a fieldless required felt type accepts any +/// felt field, while a fielded required felt type accepts only the same explicitly fielded type. +/// A TypeVarType restriction is type-only and therefore is compatible only with another +/// TypeVarType restriction; it must not inherit the wildcard behavior of ordinary type +/// unification. +bool isTemplateParamTypeCompatible(mlir::Type actualType, mlir::Type requiredType); + +/// An absent actual restriction is compatible with a fieldless required felt type, but it cannot +/// satisfy a fielded required felt type because it does not establish the field needed by that +/// restriction. It also cannot satisfy a type-only TypeVarType restriction because an unrestricted +/// binding does not establish that the eventual argument is a type. For other required types, +/// preserve the existing unrestricted-binding behavior. +bool isTemplateParamTypeCompatible(std::optional actualType, mlir::Type requiredType); + +/// Check a template argument against an optional restriction and return the representation used by +/// instantiation. With no restriction, return the argument unchanged. A type-variable restriction +/// accepts only a `TypeAttr`; a felt restriction accepts a compatible felt constant or integer, +/// preserving an explicit field when the restriction is fieldless and otherwise applying the +/// required field; and an integer-like restriction accepts a valid integer or a single-result +/// affine map that remains deferred for affine instantiation. Reject every other +/// attribute/restriction pairing. +mlir::FailureOr +materializeTemplateParamValue(mlir::Attribute actualValue, std::optional requiredType); + +/// Return `true` iff two template argument values are compatible with the same required +/// restriction. Felt constants and integer attributes are compared by value after applying the +/// required felt field, independent of APInt storage width. Deferred symbol references use the +/// generic template-parameter unifier; other values that cannot materialize as the required felt +/// are incompatible. Non-felt and absent restrictions use the generic unifier unchanged. +bool templateParamValuesUnify( + mlir::Attribute actualValue, mlir::Attribute inferredValue, + std::optional requiredType +); + /// Return `true` iff the two lists of Type instances are equivalent or could be equivalent after /// full instantiation of template parameters (if applicable within the given types). template diff --git a/lib/Dialect/Felt/IR/Attrs.cpp b/lib/Dialect/Felt/IR/Attrs.cpp index 76eca8e8cb..53f042cf64 100644 --- a/lib/Dialect/Felt/IR/Attrs.cpp +++ b/lib/Dialect/Felt/IR/Attrs.cpp @@ -18,4 +18,14 @@ StringAttr FeltConstAttr::getFieldName() const { return ft ? ft.getFieldName() : StringAttr(); } +FailureOr FeltConstAttr::materializeAs(FeltType expectedType) const { + FeltType valueType = getType(); + if (valueType.hasField() && expectedType.hasField() && valueType != expectedType) { + return failure(); + } + return FeltConstAttr::get( + getContext(), getValue(), expectedType.hasField() ? expectedType : valueType + ); +} + } // namespace llzk::felt diff --git a/lib/Dialect/Function/IR/Ops.cpp b/lib/Dialect/Function/IR/Ops.cpp index 9f94396687..23f52cecd3 100644 --- a/lib/Dialect/Function/IR/Ops.cpp +++ b/lib/Dialect/Function/IR/Ops.cpp @@ -15,9 +15,8 @@ #include "llzk/Dialect/Function/IR/Ops.h" -#include "llzk/Dialect/Felt/IR/Attrs.h" -#include "llzk/Dialect/Felt/IR/Types.h" #include "llzk/Dialect/Function/IR/Dialect.h" +#include "llzk/Dialect/Global/IR/Ops.h" #include "llzk/Dialect/LLZK/IR/AttributeHelper.h" #include "llzk/Dialect/LLZK/IR/Versioning.h" #include "llzk/Dialect/Polymorphic/IR/Types.h" @@ -43,7 +42,6 @@ #include "llzk/Dialect/Function/IR/Ops.cpp.inc" using namespace mlir; -using namespace llzk::felt; using namespace llzk::component; using namespace llzk::polymorphic; @@ -642,6 +640,7 @@ CallOp::verifyTemplateParamCompatibility(Attribute paramFromCallOp, TemplatePara if (std::optional declaredType = targetParam.getTypeOpt()) { bool compatible = false; if (auto sym = llvm::dyn_cast(paramFromCallOp)) { + bool resolvedLocal = false; if (sym.getNestedReferences().empty()) { SymbolTableCollection tables; FailureOr parentTemplate = getConstResolutionTemplate(tables, *this); @@ -651,30 +650,20 @@ CallOp::verifyTemplateParamCompatibility(Attribute paramFromCallOp, TemplatePara if (TemplateOp p = *parentTemplate) { auto binding = p.getConstNamed(sym.getRootReference()); if (binding) { - // Once we know it references a template symbol binding, assume it's compatible unless - // the optional type is present and doesn't unify with the declared type. - if (std::optional actualType = binding.getTypeOpt()) { - compatible = typesUnify(*actualType, *declaredType); - } else { - compatible = true; - } + resolvedLocal = true; + compatible = isTemplateParamTypeCompatible(binding.getTypeOpt(), *declaredType); } } } - } else if (llvm::isa(*declaredType)) { - compatible = llvm::isa(paramFromCallOp); - } else if (llvm::isa(*declaredType)) { - compatible = llvm::isa(paramFromCallOp) && - isValidConstReadType(llvm::cast(paramFromCallOp).getType()); - } else if (llvm::isa(*declaredType)) { - // Note: Just like struct type instantiation, there is no restriction on passing a - // larger value to an `i1`. The flattening pass will treat 0 as false and any other - // value as true (but give a warning if it's not 1). - compatible = llvm::isa(paramFromCallOp) && - isValidConstReadType(llvm::cast(paramFromCallOp).getType()); + if (!resolvedLocal) { + SymbolTableCollection tables; + if (auto global = lookupTopLevelSymbol(tables, sym, *this, false); + succeeded(global)) { + compatible = isTemplateParamTypeCompatible(global->get().getType(), *declaredType); + } + } } else { - // Note: `declaredType` is restricted by `isValidConstReadType()` - llvm_unreachable("inconsistent with `isValidConstReadType()`"); + compatible = succeeded(materializeTemplateParamValue(paramFromCallOp, declaredType)); } if (!compatible) { // Tested in call_with_template_params_fail.llzk @@ -707,6 +696,29 @@ LogicalResult CallOp::verifyTemplateParamsMatchInferred( const UnificationMap &unifications ) { ArrayAttr callParams = this->getTemplateParamsAttr(); + if (isNullOrEmpty(callParams)) { + for (TemplateParamOp paramOp : targetParamDefs) { + if (std::optional declaredType = paramOp.getTypeOpt(); + declaredType && llvm::isa(*declaredType)) { + continue; + } + auto it = unifications.find({FlatSymbolRefAttr::get(paramOp.getNameAttr()), Side::RHS}); + if (it == unifications.end()) { + // No inferred value means the signature did not expose this parameter to this call. + continue; + } + if (!it->second) { + return this->emitOpError().append( + "cannot infer template instantiation value for parameter \"@", paramOp.getName(), + "\" from function type signature" + ); + } + if (failed(verifyTemplateParamCompatibility(it->second, paramOp))) { + return failure(); + } + } + return success(); + } assert(!isNullOrEmpty(callParams) && "pre-condition"); assert((callParams.size() == llvm::range_size(targetParamDefs)) && "pre-condition"); @@ -718,7 +730,26 @@ LogicalResult CallOp::verifyTemplateParamsMatchInferred( } } auto it = unifications.find({FlatSymbolRefAttr::get(paramOp.getNameAttr()), Side::RHS}); - if (it != unifications.end() && !typeParamsUnify({attr}, {it->second})) { + if (it != unifications.end() && !it->second) { + return this->emitOpError().append( + "cannot infer a unique template instantiation value for parameter \"@", paramOp.getName(), + "\" from function type signature" + ); + } + if (it != unifications.end() && failed(verifyTemplateParamCompatibility(it->second, paramOp))) { + return failure(); + } + bool valuesUnify = true; + if (it != unifications.end()) { + SymbolTableCollection tables; + FailureOr resolved = + resolvedTemplateParamValuesUnify(tables, *this, attr, it->second, paramOp.getTypeOpt()); + if (failed(resolved)) { + return failure(); + } + valuesUnify = *resolved; + } + if (!valuesUnify) { // Tested in call_with_template_params_fail.llzk return this->emitOpError().append( "template instantiation value '", attr, "' for parameter \"@", paramOp.getName(), @@ -851,7 +882,7 @@ struct KnownTargetVerifier : public CallOpVerifier { auto realParams = tgtOpParent.getConstOps(); ArrayAttr callParams = callOp->getTemplateParamsAttr(); - // When there is no instantiation list, just ensure that it's not required. + // When every parameter appears in the signature, infer and validate omitted arguments. if (isNullOrEmpty(callParams)) { llvm::SmallDenseSet referencedInSignature; llzk::getSymbolsUsedIn(tgtType.getInputs(), referencedInSignature); @@ -861,7 +892,11 @@ struct KnownTargetVerifier : public CallOpVerifier { return referencedInSignature.contains(FlatSymbolRefAttr::get(p.getNameAttr())); }); if (allParamsReferenced) { - return success(); + FailureOr unifyResult = callOp->unifyTypeSignature(tgtType); + if (failed(unifyResult)) { + return failure(); + } + return callOp->verifyTemplateParamsMatchInferred(realParams, unifyResult.value()); } // Tested in call_with_template_params_fail.llzk return callOp->emitOpError().append( diff --git a/lib/Dialect/Polymorphic/IR/Ops.cpp b/lib/Dialect/Polymorphic/IR/Ops.cpp index 965b67401b..df5e01dfeb 100644 --- a/lib/Dialect/Polymorphic/IR/Ops.cpp +++ b/lib/Dialect/Polymorphic/IR/Ops.cpp @@ -276,4 +276,11 @@ LogicalResult UnifiableCastOp::verify() { return success(); } +OpFoldResult UnifiableCastOp::fold(FoldAdaptor adaptor) { + if (getInput().getType() != getResult().getType()) { + return {}; + } + return adaptor.getInput(); +} + } // namespace llzk::polymorphic diff --git a/lib/Dialect/Polymorphic/Transforms/FlatteningPass.cpp b/lib/Dialect/Polymorphic/Transforms/FlatteningPass.cpp index cf69e557c7..6763fe4081 100644 --- a/lib/Dialect/Polymorphic/Transforms/FlatteningPass.cpp +++ b/lib/Dialect/Polymorphic/Transforms/FlatteningPass.cpp @@ -99,6 +99,9 @@ static void reportDelayedDiagnostics(CallOp caller, SmallVector &&di } class ConversionTracker { + /// Exact specialization identity: source definition plus ordered concrete parameter bindings. + using FuncInstantiationKey = std::pair; + /// Published result of one successful partial-function conversion. /// /// The source operation and concrete key live in the surrounding map; these names are only the @@ -114,10 +117,15 @@ class ConversionTracker { /// Maps original remote (i.e., use site) type to new remote type. /// Note: The keys are always parameterized StructType and the values are no-parameter StructType. DenseMap structInstantiations; - /// Contains the reverse of mappings in `structInstantiations` for use in legal conversion check. + /// Maps each instantiated type to its first (canonical) source type for legal conversion checks. + /// Additional representation aliases remain in `structInstantiations` only. DenseMap reverseInstantiations; /// Tracks original free function definitions for which instantiated clones were created. DenseSet funcInstantiations; + /// Full function specializations are keyed by source definition and exact concrete bindings. + /// Generated symbol spelling is only a value because user symbols can collide with it. + /// These caches are queried during instantiation, before cleanup can erase source definitions. + DenseMap fullFuncInstantiations; /// Successful partial functions keyed by their source operation and exact concrete bindings. /// The rendered symbol names are only values; they are never used as cache identity. DenseMap> partialFuncInstantiations; @@ -135,19 +143,16 @@ class ConversionTracker { auto forwardResult = structInstantiations.try_emplace(oldType, newType); if (forwardResult.second) { - // Insertion was successful - // ASSERT: The reverse map does not contain this mapping either - assert(!reverseInstantiations.contains(newType)); - reverseInstantiations[newType] = oldType; + // Preserve the first preimage as canonical; later raw spellings are forward aliases. + reverseInstantiations.try_emplace(newType, oldType); // Set the modified flag modified = true; } else { // ASSERT: If a mapping already existed for `oldType` it must be `newType` assert(forwardResult.first->getSecond() == newType); - // ASSERT: The reverse mapping is already present as well - assert(reverseInstantiations.lookup(newType) == oldType); } - assert(structInstantiations.size() == reverseInstantiations.size()); + assert(reverseInstantiations.contains(newType)); + assert(structInstantiations.size() >= reverseInstantiations.size()); } /// Return the instantiated type of the given StructType, if any. @@ -165,6 +170,23 @@ class ConversionTracker { modified = true; } + /// Return the post-insertion symbol name for this source function and exact concrete bindings. + std::optional + getFullFuncInstantiation(FuncDefOp sourceFunc, ArrayAttr concreteParams) const { + auto it = fullFuncInstantiations.find({sourceFunc.getOperation(), concreteParams}); + return it == fullFuncInstantiations.end() ? std::nullopt : std::make_optional(it->second); + } + + /// Record a successful full specialization using its post-insertion symbol name. + void recordFullFuncInstantiation( + FuncDefOp sourceFunc, ArrayAttr concreteParams, StringAttr instantiatedName + ) { + [[maybe_unused]] auto [it, inserted] = fullFuncInstantiations.try_emplace( + {sourceFunc.getOperation(), concreteParams}, instantiatedName + ); + assert((inserted || it->second == instantiatedName) && "instantiation identity is stable"); + } + /// Return the successfully converted partial function for this exact source/key pair, if any. std::optional lookupPartialFuncInstantiation(FuncDefOp sourceFunc, ArrayAttr concreteParamKey) const { @@ -201,9 +223,12 @@ class ConversionTracker { ); } - /// No partial-function cache entry is read after cleanup starts; release its operation names at - /// that boundary so the tracker does not retain stale handles while cleanup erases templates. - void clearPartialFuncInstantiations() { partialFuncInstantiations.clear(); } + /// No function-instantiation cache entry is read after cleanup starts. Clear source-operation + /// keys before cleanup can erase their definitions. + void clearFuncInstantiations() { + fullFuncInstantiations.clear(); + partialFuncInstantiations.clear(); + } /// Collect the fully-qualified names of all structs and free functions that were instantiated. DenseSet getInstantiatedDefinitionNames() const { @@ -391,11 +416,158 @@ class ClonedBodyConstReadOpPattern LogicalResult handleRewrite( Attribute, ConstReadOp op, OpAdaptor, ConversionPatternRewriter &rewriter, FeltConstAttr a ) const { - replaceOpWithNewOp(rewriter, op, a); + Type origResTy = op.getType(); + Type newResTy = getTypeConverter()->convertType(origResTy); + FeltType feltType = llvm::dyn_cast_or_null(newResTy); + if (!feltType) { + return op->emitOpError().append( + "expected a concrete felt result type after conversion, but found ", + newResTy ? newResTy : origResTy + ); + } + + FailureOr materialized = + materializeTemplateParamValue(a, std::optional(feltType)); + if (failed(materialized)) { + return op->emitOpError().append( + "felt constant ", a, " is incompatible with converted result type ", feltType + ); + } + replaceOpWithNewOp(rewriter, op, llvm::cast(*materialized)); return success(); } }; +/// Apply known template bindings throughout types and type-valued attributes. This converts scalar +/// type variables, array dimensions and element types, parameterized struct arguments, and POD +/// record types. Unbound parameters and otherwise unchanged types retain their original form. +class TemplateParamTypeConverter : public TypeConverter { + const DenseMap ¶mNameToValue; + +protected: + Attribute convertIfPossible(Attribute attr) const { + auto res = paramNameToValue.find(attr); + return (res != paramNameToValue.end()) ? res->second : attr; + } + +public: + explicit TemplateParamTypeConverter(const DenseMap ¶mNameToConcrete) + : TypeConverter(), paramNameToValue(paramNameToConcrete) { + addConversion([](Type type) { return type; }); + addConversion([this](TypeVarType inputTy) -> Type { + if (TypeAttr tyAttr = llvm::dyn_cast(convertIfPossible(inputTy.getNameRef()))) { + Type convertedType = tyAttr.getValue(); + if (isConcreteType(convertedType)) { + return convertedType; + } + } + return inputTy; + }); + + addConversion([this](ArrayType inputTy) { + SmallVector updatedDims; + bool changed = false; + for (Attribute dim : inputTy.getDimensionSizes()) { + Attribute converted = convertIfPossible(dim); + updatedDims.push_back(converted); + changed |= converted != dim; + } + Type updatedElement = convertType(inputTy.getElementType()); + if (!changed && updatedElement == inputTy.getElementType()) { + return inputTy; + } + return flattenArrayElementType( + inputTy.cloneWith(inputTy.getElementType(), updatedDims), updatedElement + ); + }); + + addConversion([this](StructType inputTy) -> StructType { + ArrayAttr params = inputTy.getParams(); + if (!params) { + return inputTy; + } + SmallVector updatedParams; + bool changed = false; + for (Attribute param : params) { + Attribute converted = convertAttr(param); + updatedParams.push_back(converted); + changed |= converted != param; + } + return changed ? getStructTypeWithParams( + inputTy.getNameRef(), inputTy.getContext(), updatedParams + ) + : inputTy; + }); + + addConversion([this](pod::PodType inputTy) -> pod::PodType { + SmallVector updatedRecords; + bool changed = false; + for (pod::RecordAttr record : inputTy.getRecords()) { + Type converted = convertType(record.getType()); + updatedRecords.push_back( + converted == record.getType() + ? record + : pod::RecordAttr::get(inputTy.getContext(), record.getName(), converted) + ); + changed |= converted != record.getType(); + } + return changed ? pod::PodType::get(inputTy.getContext(), updatedRecords) : inputTy; + }); + } + + /// Recursively convert a type-valued attribute; otherwise replace an exact bound parameter. + Attribute convertAttr(Attribute attr) const { + if (TypeAttr tyAttr = llvm::dyn_cast(attr)) { + Type convertedTy = convertType(tyAttr.getValue()); + if (convertedTy != tyAttr.getValue()) { + return TypeAttr::get(convertedTy); + } + } + return convertIfPossible(attr); + } + + bool containsParam(Attribute nameAttr) const { return paramNameToValue.contains(nameAttr); } + const DenseMap &getParamMap() const { return paramNameToValue; } +}; + +/// Clone a target-used template expression and apply every currently concrete value and type +/// binding. A reduced template no longer owns the removed parameter declarations, so its retained +/// expression must contain neither reads nor type variables for those parameters. Return an empty +/// result when a known value still has a non-concrete converted type, blocking that specialization +/// attempt rather than discarding a binding that cannot yet be materialized. +static FailureOr> cloneDeferredExpr( + TemplateExprOp exprOp, const DenseMap ¶mNameToConcrete, + SmallVector &diagnostics +) { + MLIRContext *ctx = exprOp.getContext(); + TemplateParamTypeConverter tyConv(paramNameToConcrete); + WalkResult blocked = exprOp.walk([&](ConstReadOp readOp) { + if (!paramNameToConcrete.contains(readOp.getConstNameAttr())) { + return WalkResult::advance(); + } + Type convertedType = tyConv.convertType(readOp.getType()); + return (!convertedType || !isConcreteType(convertedType)) ? WalkResult::interrupt() + : WalkResult::advance(); + }); + if (blocked.wasInterrupted()) { + return std::optional(); + } + + TemplateExprOp clonedExpr = llvm::cast(exprOp->clone()); + ConversionTarget target = newConverterDefinedTarget<>(tyConv, ctx); + target.addDynamicallyLegalOp([&](ConstReadOp op) { + return !paramNameToConcrete.contains(op.getConstNameAttr()) && defaultLegalityCheck(tyConv, op); + }); + + RewritePatternSet patterns = newGeneralRewritePatternSet<>(tyConv, ctx, target); + patterns.add(tyConv, ctx, paramNameToConcrete, diagnostics); + if (failed(applyFullConversion(clonedExpr, target, std::move(patterns)))) { + clonedExpr->destroy(); + return failure(); + } + return std::make_optional(clonedExpr); +} + /// Patterns can use this listener and call notifyMatchFailure(..) for failures where the entire /// pass must fail, i.e., where instantiation would introduce an illegal type conversion. struct MatchFailureListener : public RewriterBase::Listener { @@ -452,10 +624,30 @@ convertCalleeSymRefs(SymbolRefAttr callee, const DenseMap return asSymbolRefAttr(newPieces); } +/// Rewrite callees in a cloned target using concrete type bindings. Materialize explicit +/// nested-call arguments for removed bindings before the clone enters a reduced template or a +/// parent module. static void convertCalleesInPlace(Operation *op, const DenseMap ¶mNameToValue) { - op->walk([¶mNameToValue](CallOp callOp) { + TemplateParamTypeConverter tyConv(paramNameToValue); + op->walk([¶mNameToValue, &tyConv](CallOp callOp) { callOp.setCalleeAttr(convertCalleeSymRefs(callOp.getCalleeAttr(), paramNameToValue)); + + ArrayAttr templateParams = callOp.getTemplateParamsAttr(); + if (!templateParams) { + return; + } + SmallVector convertedParams; + convertedParams.reserve(templateParams.size()); + bool changed = false; + for (Attribute param : templateParams) { + Attribute converted = tyConv.convertAttr(param); + convertedParams.push_back(converted); + changed |= converted != param; + } + if (changed) { + callOp.setTemplateParamsAttr(ArrayAttr::get(callOp.getContext(), convertedParams)); + } }); } @@ -471,24 +663,36 @@ static bool calleeReferencesTemplateParam(CallOp op) { return parentTemplate.hasConstNamed(callee.getRootReference()); } -/// Attempt to evaluate the concrete result of a single `TemplateExprOp` expression given -/// the currently-known concrete param values in `paramNameToConcrete`. Returns the result -/// attribute if all referenced params are concrete and all operations in the body can be -/// constant-folded; otherwise returns `std::nullopt`. -static std::optional +/// Evaluate a single template expression. An unresolved parameter defers evaluation; malformed, +/// incompatible, or non-foldable concrete expressions are semantic errors. +static FailureOr> evaluateExpr(TemplateExprOp exprOp, const DenseMap ¶mNameToConcrete) { + // Deferral depends on the expression's complete parameter set, not operation order. Do not + // diagnose a non-foldable prefix while a later read still requires partial instantiation. + WalkResult unresolvedParam = exprOp.walk([&](ConstReadOp op) { + return paramNameToConcrete.contains(op.getConstNameAttr()) ? WalkResult::advance() + : WalkResult::interrupt(); + }); + if (unresolvedParam.wasInterrupted()) { + return std::optional(); + } + // Map from SSA value in the expr body to its concrete Attribute. DenseMap valueMap; for (Operation &bodyOp : exprOp.getInitializerRegion().front()) { if (auto yieldOp = llvm::dyn_cast(bodyOp)) { auto it = valueMap.find(yieldOp.getVal()); - return it != valueMap.end() ? std::make_optional(it->second) : std::nullopt; + if (it != valueMap.end()) { + return std::make_optional(it->second); + } + yieldOp.emitOpError("cannot evaluate yielded value as a concrete template constant"); + return failure(); } if (auto constReadOp = llvm::dyn_cast(bodyOp)) { auto it = paramNameToConcrete.find(constReadOp.getConstNameAttr()); if (it == paramNameToConcrete.end()) { - return std::nullopt; // a referenced param is not concrete + return std::optional(); } // If the attribute type is `FeltType` but it's stored as an IntegerAttr, promote to // a `FeltConstAttr`. @@ -508,51 +712,103 @@ evaluateExpr(TemplateExprOp exprOp, const DenseMap ¶mN for (Value operand : bodyOp.getOperands()) { auto it = valueMap.find(operand); if (it == valueMap.end()) { - return std::nullopt; // operand not known as a constant + bodyOp.emitOpError("cannot evaluate operand as a concrete template constant"); + return failure(); } operandAttrs.push_back(it->second); } // Try constant folding. SmallVector foldResults; - if (succeeded(bodyOp.fold(operandAttrs, foldResults)) && - foldResults.size() == bodyOp.getNumResults()) { - for (auto [result, fr] : llvm::zip_equal(bodyOp.getResults(), foldResults)) { - if (Attribute a = llvm::dyn_cast(fr)) { - valueMap[result] = a; - } else { - return std::nullopt; - } + if (failed(bodyOp.fold(operandAttrs, foldResults)) || + foldResults.size() != bodyOp.getNumResults()) { + bodyOp.emitOpError("cannot fold concrete template expression"); + return failure(); + } + for (auto [result, fr] : llvm::zip_equal(bodyOp.getResults(), foldResults)) { + if (Attribute a = llvm::dyn_cast(fr)) { + valueMap[result] = a; + } else { + bodyOp.emitOpError("template expression fold did not produce a constant attribute"); + return failure(); } } } - return std::nullopt; // no YieldOp found (shouldn't happen in a valid expr) + exprOp.emitOpError("initializer has no yield operation"); + return failure(); } -/// Evaluate all `TemplateExprOp`s in `templateOp` that can be computed from the currently-known -/// concrete param values in `paramNameToConcrete`, and add their results to the map. -/// Exprs whose operands are not all concrete are silently skipped (partial instantiation). -static void -evaluateTemplateExprs(TemplateOp templateOp, DenseMap ¶mNameToConcrete) { +/// Return whether `target` may use `exprOp`. Symbol-use analysis stops at symbol-table boundaries, +/// so inspect target regions separately. An unknown result is conservatively treated as a use. +static bool targetMayUseTemplateExpr(Operation *target, TemplateExprOp exprOp) { + if (!symbolKnownUseEmpty(exprOp.getOperation(), target)) { + return true; + } + return llvm::any_of(target->getRegions(), [&](Region ®ion) { + return !symbolKnownUseEmpty(exprOp.getOperation(), ®ion); + }); +} + +/// Evaluate target-used `TemplateExprOp`s whose dependencies are concrete, adding their values to +/// `paramNameToConcrete`. Skip expressions unused by `target`. Return normalized detached clones +/// for expressions that still depend on remaining parameters; the caller must insert or destroy +/// every returned clone. Any concrete but malformed or non-foldable expression is a failure. +static FailureOr> evaluateTemplateExprs( + TemplateOp templateOp, Operation *target, DenseMap ¶mNameToConcrete, + SmallVector &deferredExprDiagnostics +) { LLVM_DEBUG( llvm::dbgs() << "[evaluateTemplateExprs] before: " << debug::toStringList(paramNameToConcrete) << '\n' ); + SmallVector deferredExprs; + auto destroyDeferredExprs = [&]() { + for (TemplateExprOp exprOp : deferredExprs) { + exprOp->destroy(); + } + deferredExprs.clear(); + }; for (TemplateExprOp exprOp : templateOp.getConstOps()) { - std::optional result = evaluateExpr(exprOp, paramNameToConcrete); - if (result.has_value()) { + if (!targetMayUseTemplateExpr(target, exprOp)) { + continue; + } + // Evaluation and preservation must observe the same concrete type substitutions. In + // particular, a type-variable binding can make an otherwise non-foldable cast an identity + // cast, so folding the original expression would be route-dependent. + FailureOr> normalizedExpr = + cloneDeferredExpr(exprOp, paramNameToConcrete, deferredExprDiagnostics); + if (failed(normalizedExpr) || !normalizedExpr->has_value()) { + destroyDeferredExprs(); + return failure(); + } + TemplateExprOp normalizedExprOp = **normalizedExpr; + FailureOr> result = + evaluateExpr(normalizedExprOp, paramNameToConcrete); + if (failed(result)) { + normalizedExprOp->destroy(); + destroyDeferredExprs(); + return failure(); + } + if (*result) { + normalizedExprOp->destroy(); + Attribute value = result->value(); auto exprNameAttr = FlatSymbolRefAttr::get(exprOp.getSymNameAttr()); - paramNameToConcrete.try_emplace(exprNameAttr, *result); + paramNameToConcrete.try_emplace(exprNameAttr, value); LLVM_DEBUG( llvm::dbgs() << "[evaluateTemplateExprs] expr @" << exprOp.getSymName() - << " evaluated to " << *result << '\n' + << " evaluated to " << value << '\n' ); + } else { + // Keep the normalized detached clone. The caller transfers it into the reduced template, + // so later specialization starts from the same representation that was just evaluated. + deferredExprs.push_back(normalizedExprOp); } } LLVM_DEBUG( llvm::dbgs() << "[evaluateTemplateExprs] after: " << debug::toStringList(paramNameToConcrete) << '\n' ); + return deferredExprs; } static inline bool tableOffsetIsntSymbol(MemberReadOp op) { @@ -617,15 +873,9 @@ class StructCloner { SymbolTableCollection symTables; bool reportMissing = true; - class MappedTypeConverter : public TypeConverter { + class MappedTypeConverter : public TemplateParamTypeConverter { StructType origTy; StructType newTy; - const DenseMap ¶mNameToValue; - - inline Attribute convertIfPossible(Attribute a) const { - auto res = this->paramNameToValue.find(a); - return (res != this->paramNameToValue.end()) ? res->second : a; - } public: MappedTypeConverter( @@ -633,10 +883,8 @@ class StructCloner { /// Instantiated values for the parameter names in `originalType` const DenseMap ¶mNameToInstantiatedValue ) - : TypeConverter(), origTy(originalType), newTy(newType), - paramNameToValue(paramNameToInstantiatedValue) { - - addConversion([](Type inputTy) { return inputTy; }); + : TemplateParamTypeConverter(paramNameToInstantiatedValue), origTy(originalType), + newTy(newType) { addConversion([this](StructType inputTy) { LLVM_DEBUG(llvm::dbgs() << "[MappedTypeConverter] convert " << inputTy << '\n'); @@ -649,11 +897,7 @@ class StructCloner { if (ArrayAttr inputTyParams = inputTy.getParams()) { SmallVector updated; for (Attribute a : inputTyParams) { - if (TypeAttr ta = dyn_cast(a)) { - updated.push_back(TypeAttr::get(this->convertType(ta.getValue()))); - } else { - updated.push_back(convertIfPossible(a)); - } + updated.push_back(convertAttr(a)); } return getStructTypeWithParams(inputTy.getNameRef(), inputTy.getContext(), updated); } @@ -674,20 +918,6 @@ class StructCloner { // Otherwise, return the type unchanged return inputTy; }); - - addConversion([this](TypeVarType inputTy) -> Type { - // Check for replacement of parameter symbol name with a concrete type - if (TypeAttr tyAttr = llvm::dyn_cast(convertIfPossible(inputTy.getNameRef()))) { - Type convertedType = tyAttr.getValue(); - // Use the new type unless it contains a TypeVarType because a TypeVarType from a - // different struct references a parameter name from that other struct, not from the - // current struct so the reference would be invalid. - if (isConcreteType(convertedType)) { - return convertedType; - } - } - return inputTy; - }); } }; @@ -715,19 +945,34 @@ class StructCloner { // Reduced from `typeAtCallerParams` to contain only the non-concrete Attributes. ArrayAttr reducedCallerParams = nullptr; SmallVector nonConcreteParams; + SmallVector canonicalCallerParams; { ArrayAttr paramNames = typeAtDef.getParams(); // pre-conditions assert(!isNullOrEmpty(paramNames)); assert(paramNames.size() == typeAtCallerParams.size()); + auto paramOps = parentTemplate.getConstOps(); + assert(paramNames.size() == llvm::range_size(paramOps)); + canonicalCallerParams.reserve(paramNames.size()); - for (size_t i = 0, e = paramNames.size(); i < e; ++i) { - Attribute next = typeAtCallerParams[i]; + for (auto [paramName, paramOp, next] : + llvm::zip_equal(paramNames, paramOps, typeAtCallerParams)) { if (isConcreteAttr(next)) { - paramNameToConcrete[paramNames[i]] = next; + FailureOr normalized = + materializeTemplateParamValue(next, paramOp.getTypeOpt()); + if (failed(normalized)) { + origStruct.emitOpError().append( + "cannot materialize instantiation value '", next, "' for parameter \"@", + paramOp.getName(), '"' + ); + return failure(); + } + paramNameToConcrete[paramName] = *normalized; + canonicalCallerParams.push_back(*normalized); } else { nonConcreteParams.push_back(next); + canonicalCallerParams.push_back(next); } } // post-conditions @@ -742,6 +987,12 @@ class StructCloner { } } + StructType canonicalCallerType = + StructType::get(typeAtCaller.getNameRef(), ArrayAttr::get(ctx, canonicalCallerParams)); + if (auto cached = tracker_.getInstantiation(canonicalCallerType)) { + return *cached; + } + FailureOr layoutResult = buildInstantiationLayout(parentTemplate, ArrayAttr(), paramNameToConcrete); if (failed(layoutResult)) { @@ -756,11 +1007,29 @@ class StructCloner { // Evaluate any poly.expr symbols whose param dependencies are now concrete; add them to the // map so ClonedBodyConstReadOpPattern can replace uses of those symbols too. - evaluateTemplateExprs(parentTemplate, paramNameToConcrete); + SmallVector deferredExprDiagnostics; + FailureOr> exprEvaluation = evaluateTemplateExprs( + parentTemplate, origStruct.getOperation(), paramNameToConcrete, deferredExprDiagnostics + ); + if (failed(exprEvaluation)) { + return failure(); + } + SmallVector deferredExprs = std::move(*exprEvaluation); + if (layout.remainingNames.empty() && !deferredExprs.empty()) { + deferredExprs.front().emitOpError( + "cannot complete instantiation while a template expression remains deferred" + ); + for (TemplateExprOp exprOp : deferredExprs) { + exprOp->destroy(); + } + return failure(); + } // Clone the original struct. StructDefOp newStruct = origStruct.clone(); convertCalleesInPlace(newStruct, paramNameToConcrete); + // Keep the inserted owner so a failed body conversion cannot publish a partial clone. + Operation *insertedOwner = nullptr; if (layout.remainingNames.empty()) { // FULL INSTANTIATION CASE // Set name of the new struct by prepending its name with instantiated template name. newStruct.setSymName( @@ -769,6 +1038,7 @@ class StructCloner { // Insert 'newStruct' into the parent ModuleOp of the original TemplateOp. Use the // `SymbolTable::insert()` function so that the name will be made unique if necessary. symTables.getSymbolTable(parentModule).insert(newStruct, Block::iterator(parentTemplate)); + insertedOwner = newStruct.getOperation(); // Drop the old template name from the list. typeAtCallerSymPieces.pop_back(); } else { // PARTIAL INSTANTIATION CASE @@ -788,11 +1058,16 @@ class StructCloner { assert(symOp && "symbol must exist"); newTemplate.insert(newTemplate.begin(), symOp->clone()); } + for (TemplateExprOp exprOp : deferredExprs) { + newTemplate.getBodyRegion().front().push_back(exprOp.getOperation()); + } - // Insert the struct into the template and the template into the module. Use the - // `SymbolTable::insert()` function so that the name will be made unique if necessary. - symTables.getSymbolTable(newTemplate).insert(newStruct); + // Insert the struct into the detached template with a local table. The long-lived + // collection must not cache a table for a prospective owner that may be erased on failure. + SymbolTable newTemplateSymbols(newTemplate); + newTemplateSymbols.insert(newStruct); symTables.getSymbolTable(parentModule).insert(newTemplate, Block::iterator(parentTemplate)); + insertedOwner = newTemplate.getOperation(); // Replace the old template name in the list with the new one (get template name after // symbol table insertion since it may be modified to make it unique). @@ -815,6 +1090,8 @@ class StructCloner { llvm::dbgs() << "[StructCloner] cloned remote type: " << newRemoteType << '\n'; }); + SmallVector conversionDiagnostics; + // Within the new struct, replace all references to the original StructType (i.e., the // locally-parameterized version) with the new locally-parameterized StructType, // and replace all uses of the removed struct parameters with the concrete values. @@ -828,13 +1105,30 @@ class StructCloner { RewritePatternSet patterns = newGeneralRewritePatternSet(tyConv, ctx, target); patterns.add( - tyConv, ctx, paramNameToConcrete, tracker_.delayedDiagnosticSet(newLocalType) + tyConv, ctx, paramNameToConcrete, conversionDiagnostics ); patterns.add(tyConv, ctx, paramNameToConcrete); if (failed(applyFullConversion(newStruct, target, std::move(patterns)))) { LLVM_DEBUG(llvm::dbgs() << "[StructCloner] instantiating body of struct failed \n"); + // Erase the published owner through its parent table so the block and its symbol-table entry + // are removed together. A partial template's detached table is local and has already died. + symTables.getSymbolTable(parentModule).erase(insertedOwner); return failure(); } + + // Publish diagnostics only after the generated owner has passed conversion. + if (!deferredExprDiagnostics.empty() || !conversionDiagnostics.empty()) { + SmallVector &diagnostics = tracker_.delayedDiagnosticSet(newLocalType); + diagnostics.append( + std::make_move_iterator(deferredExprDiagnostics.begin()), + std::make_move_iterator(deferredExprDiagnostics.end()) + ); + diagnostics.append( + std::make_move_iterator(conversionDiagnostics.begin()), + std::make_move_iterator(conversionDiagnostics.end()) + ); + } + tracker_.recordInstantiation(canonicalCallerType, newRemoteType); return newRemoteType; } @@ -901,6 +1195,8 @@ class ParameterizedStructUseTypeConverter : public TypeConverter { } }; +/// Rebuild struct `compute` and `constrain` calls after their struct types change. Retarget the +/// callee to the converted struct while preserving affine-map and explicit template arguments. class CallStructFuncPattern : public OpConversionPattern { ConversionTracker &tracker_; @@ -942,9 +1238,12 @@ class CallStructFuncPattern : public OpConversionPattern { } LLVM_DEBUG(llvm::dbgs() << "[CallStructFuncPattern] replaced " << op); + ArrayAttr templateParamsAttr = op.getTemplateParamsAttr(); + ArrayRef templateParams = + templateParamsAttr ? templateParamsAttr.getValue() : ArrayRef(); CallOp newOp = replaceOpWithNewOp( rewriter, op, newResultTypes, calleeAttr, adapter.getMapOperands(), - op.getNumDimsPerMapAttr(), adapter.getArgOperands() + op.getNumDimsPerMapAttr(), adapter.getArgOperands(), templateParams ); (void)newOp; // tell compiler it's intentionally unused in release builds LLVM_DEBUG(llvm::dbgs() << " with " << newOp << '\n'); @@ -1026,94 +1325,6 @@ LogicalResult instantiateMainStruct(ModuleOp modOp, ConversionTracker &tracker) namespace Step2_InstantiateFunctions { -/// TypeConverter for function instantiation that replaces TypeVarType and symbolic -/// ArrayType/StructType parameters with their concrete values determined by unification. -class FuncInstTypeConverter : public TypeConverter { - DenseMap paramNameToValue; - - Attribute convertIfPossible(Attribute a) const { - auto res = paramNameToValue.find(a); - return (res != paramNameToValue.end()) ? res->second : a; - } - -public: - explicit FuncInstTypeConverter(DenseMap paramNameToConcrete) - : TypeConverter(), paramNameToValue(std::move(paramNameToConcrete)) { - addConversion([](Type t) { return t; }); - - addConversion([this](TypeVarType inputTy) -> Type { - if (TypeAttr tyAttr = llvm::dyn_cast(convertIfPossible(inputTy.getNameRef()))) { - Type convertedType = tyAttr.getValue(); - if (isConcreteType(convertedType)) { - return convertedType; - } - } - return inputTy; - }); - - addConversion([this](ArrayType inputTy) { - SmallVector updated; - bool changed = false; - for (Attribute a : inputTy.getDimensionSizes()) { - Attribute converted = convertIfPossible(a); - updated.push_back(converted); - if (converted != a) { - changed = true; - } - } - Type newElemTy = this->convertType(inputTy.getElementType()); - if (!changed && newElemTy == inputTy.getElementType()) { - return inputTy; - } - return flattenArrayElementType( - inputTy.cloneWith(inputTy.getElementType(), updated), newElemTy - ); - }); - - addConversion([this](StructType inputTy) -> StructType { - if (ArrayAttr params = inputTy.getParams()) { - SmallVector updated; - bool changed = false; - for (Attribute a : params) { - if (TypeAttr ta = dyn_cast(a)) { - Type newTy = this->convertType(ta.getValue()); - if (newTy != ta.getValue()) { - updated.push_back(TypeAttr::get(newTy)); - changed = true; - continue; - } - } else { - Attribute converted = convertIfPossible(a); - if (converted != a) { - updated.push_back(converted); - changed = true; - continue; - } - } - updated.push_back(a); - } - if (changed) { - return getStructTypeWithParams(inputTy.getNameRef(), inputTy.getContext(), updated); - } - } - return inputTy; - }); - } - - Attribute convertAttr(Attribute attr) const { - if (TypeAttr tyAttr = llvm::dyn_cast(attr)) { - Type convertedTy = convertType(tyAttr.getValue()); - if (convertedTy != tyAttr.getValue()) { - return TypeAttr::get(convertedTy); - } - } - return convertIfPossible(attr); - } - - bool containsParam(Attribute nameAttr) const { return paramNameToValue.contains(nameAttr); } - const DenseMap &getParamMap() const { return paramNameToValue; } -}; - /// Return the callee-side unification-derived value for a template parameter, if any. inline static std::optional inferUnifiedParam(const UnificationMap &unifyResult, SymbolRefAttr paramName) { @@ -1160,7 +1371,7 @@ class WildcardTypeBodyInferer final { } activeInferences_.emplace_back(func.getOperation(), paramName); - FuncInstTypeConverter tyConv((paramNameToConcrete_)); + TemplateParamTypeConverter tyConv(paramNameToConcrete_); std::optional inferred; bool ambiguous = false; @@ -1250,7 +1461,7 @@ class WildcardTypeBodyInferer final { private: std::optional inferFromExplicitNestedCallParams( CallOp nestedCall, TemplateOp nestedTemplate, FlatSymbolRefAttr nestedParamName, - const FuncInstTypeConverter &tyConv + const TemplateParamTypeConverter &tyConv ) const { ArrayAttr nestedCallParams = nestedCall.getTemplateParamsAttr(); if (isNullOrEmpty(nestedCallParams)) { @@ -1309,14 +1520,14 @@ class ClonedBodyArrayWriteOpPattern final : public OpConversionPattern ¶mNameToConcrete ) { MLIRContext *ctx = op.getContext(); - FuncInstTypeConverter tyConv(paramNameToConcrete); + TemplateParamTypeConverter tyConv(paramNameToConcrete); ConversionTarget target = newConverterDefinedTarget<>(tyConv, ctx, tableOffsetIsntSymbol); target.addDynamicallyLegalOp([&tyConv](ConstReadOp p) { // Legal if it's not in the map of concrete attribute instantiations @@ -1333,15 +1544,20 @@ static LogicalResult applyBodyConversions( return failure(); } LLVM_DEBUG(llvm::dbgs() << "[InstantiateFuncAtCallOp] instantiated clone: " << newFunc << '\n'); - ::reportDelayedDiagnostics(op, std::move(delayedDiagnostics)); - SymbolTableCollection tables; WalkResult res = newFunc.walk([&tables](CallOp nestedCall) { return WalkResult(nestedCall.verifySymbolUses(tables)); }); - return failure(res.wasInterrupted()); + if (res.wasInterrupted()) { + return failure(); + } + ::reportDelayedDiagnostics(op, std::move(delayedDiagnostics)); + return success(); } +/// Specialize calls whose target is a free function inside a `poly.template`. The rewrite +/// materializes every known binding, creates a full clone or a reduced template when concrete +/// progress is possible, and leaves the call unchanged when no parameter can yet be specialized. class InstantiateFuncAtCallOp final : public OpRewritePattern { ConversionTracker &tracker_; @@ -1406,11 +1622,21 @@ class InstantiateFuncAtCallOp final : public OpRewritePattern { return failure(); } - evaluateTemplateExprs(parentTemplate, paramNameToConcrete); + SmallVector deferredExprDiagnostics; + FailureOr> exprEvaluation = evaluateTemplateExprs( + parentTemplate, callTgt.getOperation(), paramNameToConcrete, deferredExprDiagnostics + ); + if (failed(exprEvaluation)) { + return failure(); + } + SmallVector deferredExprs = std::move(*exprEvaluation); FailureOr layoutResult = buildInstantiationLayout(parentTemplate, op.getTemplateParamsAttr(), paramNameToConcrete); if (failed(layoutResult)) { + for (TemplateExprOp exprOp : deferredExprs) { + exprOp->destroy(); + } return failure(); } InstantiationLayout layout = std::move(*layoutResult); @@ -1418,20 +1644,34 @@ class InstantiateFuncAtCallOp final : public OpRewritePattern { assert(parentModule && "TemplateOp must be nested in a ModuleOp"); SymbolRefAttr originalCalleeAttr = op.getCalleeAttr(); + if (layout.remainingNames.empty() && !deferredExprs.empty()) { + deferredExprs.front().emitOpError( + "cannot complete instantiation while a template expression remains deferred" + ); + for (TemplateExprOp exprOp : deferredExprs) { + exprOp->destroy(); + } + return failure(); + } FailureOr newCalleeAttr = layout.remainingNames.empty() ? instantiateFully( op, rewriter, symTables, callTgt, parentTemplate, parentModule, - layout.templateNameWithAttrs, paramNameToConcrete + layout.templateNameWithAttrs, layout.concreteParamKey, paramNameToConcrete, + tracker_ ) : instantiatePartially( op, rewriter, symTables, callTgt, parentTemplate, parentModule, layout, - paramNameToConcrete, tracker_ + paramNameToConcrete, tracker_, deferredExprs, deferredExprDiagnostics ); if (failed(newCalleeAttr)) { return failure(); } + if (layout.remainingNames.empty()) { + ::reportDelayedDiagnostics(op, std::move(deferredExprDiagnostics)); + } + tracker_.recordInstantiation(originalCalleeAttr); // Update the CallOp to point to the instantiated function and mark the module as modified. @@ -1478,11 +1718,19 @@ class InstantiateFuncAtCallOp final : public OpRewritePattern { ); auto recordConcreteParam = [&](FlatSymbolRefAttr paramName, TemplateParamOp paramOp, - Attribute concreteValue) { + Attribute concreteValue) -> LogicalResult { if (failed(op.verifyTemplateParamCompatibility(concreteValue, paramOp))) { return failIncompatibleInferredParam(op, rewriter, paramName, paramOp); } - paramNameToConcrete[paramName] = concreteValue; + FailureOr normalized = + materializeTemplateParamValue(concreteValue, paramOp.getTypeOpt()); + if (failed(normalized)) { + return op.emitOpError().append( + "instantiation value '", concreteValue, "' is not compatible with parameter \"@", + paramOp.getName(), "\" type restriction ", *paramOp.getTypeOpt() + ); + } + paramNameToConcrete[paramName] = *normalized; return success(); }; @@ -1537,7 +1785,9 @@ class InstantiateFuncAtCallOp final : public OpRewritePattern { auto paramName = FlatSymbolRefAttr::get(paramOp.getSymNameAttr()); AttrConcreteness classification = classifyAttrConcreteness(attr); if (classification == AttrConcreteness::Concrete) { - paramNameToConcrete[paramName] = attr; + if (failed(recordConcreteParam(paramName, paramOp, attr))) { + return failure(); + } continue; } @@ -1579,17 +1829,27 @@ class InstantiateFuncAtCallOp final : public OpRewritePattern { } /// Create or reuse a fully-instantiated clone in the parent module and return the rewritten - /// module-level callee reference. + /// module-level callee reference. Reuse is keyed by the source function and exact ordered + /// concrete bindings; the rendered template name is only a preferred symbol name and may be + /// changed by SymbolTable insertion. static FailureOr instantiateFully( CallOp op, PatternRewriter &rewriter, SymbolTableCollection &symTables, FuncDefOp callTgt, TemplateOp parentTemplate, ModuleOp parentModule, StringRef templateNameWithAttrs, - const DenseMap ¶mNameToConcrete + ArrayAttr concreteParamKey, const DenseMap ¶mNameToConcrete, + ConversionTracker &tracker ) { MLIRContext *ctx = op.getContext(); std::string newFuncName = (mlir::Twine(templateNameWithAttrs) + "_" + callTgt.getSymName()).str(); StringRef actualNewFuncName = newFuncName; - if (!symTables.getSymbolTable(parentModule).lookup(newFuncName)) { + if (std::optional cached = + tracker.getFullFuncInstantiation(callTgt, concreteParamKey)) { + actualNewFuncName = cached->getValue(); + LLVM_DEBUG( + llvm::dbgs() << "[InstantiateFuncAtCallOp] reusing full instantiation function: " + << actualNewFuncName << '\n' + ); + } else { FuncDefOp newFunc = callTgt.clone(); newFunc.setSymName(newFuncName); convertCalleesInPlace(newFunc, paramNameToConcrete); @@ -1605,16 +1865,14 @@ class InstantiateFuncAtCallOp final : public OpRewritePattern { llvm::dbgs() << "[InstantiateFuncAtCallOp] body conversion failed for " << actualNewFuncName << '\n' ); - newFunc->erase(); + // Remove the operation through the table that inserted it so a failed clone leaves no + // stale symbol entry for a later specialization with the same preferred name. + symTables.getSymbolTable(parentModule).erase(newFunc); return rewriter.notifyMatchFailure(op, [&](Diagnostic &diag) { diag.append("failure while creating instantiated function '", actualNewFuncName, '\''); }); } - } else { - LLVM_DEBUG( - llvm::dbgs() << "[InstantiateFuncAtCallOp] reusing full instantiation function: " - << actualNewFuncName << '\n' - ); + tracker.recordFullFuncInstantiation(callTgt, concreteParamKey, newFunc.getSymNameAttr()); } // Callee: drop template & original function names, add the new module-level function name. @@ -1635,7 +1893,8 @@ class InstantiateFuncAtCallOp final : public OpRewritePattern { static FailureOr instantiatePartially( CallOp op, PatternRewriter &rewriter, SymbolTableCollection &symTables, FuncDefOp callTgt, TemplateOp parentTemplate, ModuleOp parentModule, const InstantiationLayout &layout, - const DenseMap ¶mNameToConcrete, ConversionTracker &tracker + const DenseMap ¶mNameToConcrete, ConversionTracker &tracker, + ArrayRef deferredExprs, SmallVector &deferredExprDiagnostics ) { if (auto cached = tracker.lookupPartialFuncInstantiation(callTgt, layout.concreteParamKey)) { SmallVector symPieces = getPieces(op.getCalleeAttr()); @@ -1651,6 +1910,10 @@ class InstantiateFuncAtCallOp final : public OpRewritePattern { llvm::dbgs() << "[InstantiateFuncAtCallOp] reusing partial instantiation: " << cachedCallee << '\n' ); + for (TemplateExprOp exprOp : deferredExprs) { + exprOp->destroy(); + } + ::reportDelayedDiagnostics(op, std::move(deferredExprDiagnostics)); return cachedCallee; } TemplateOp newTemplate = parentTemplate.cloneWithoutRegions(); @@ -1666,6 +1929,9 @@ class InstantiateFuncAtCallOp final : public OpRewritePattern { assert(paramOp && "symbol must exist"); newTemplateBody.push_back(paramOp->clone()); } + for (TemplateExprOp exprOp : deferredExprs) { + newTemplateBody.push_back(exprOp.getOperation()); + } // Clone and partially convert the function (concretize only the concrete params). FuncDefOp newFunc = callTgt.clone(); @@ -1673,7 +1939,12 @@ class InstantiateFuncAtCallOp final : public OpRewritePattern { // Insert before body conversion so nested concrete callees verify from the root module. Use // SymbolTable::insert() so both physical symbol names are unique if necessary. - symTables.getSymbolTable(newTemplate).insert(newFunc); + // Use a local table for the detached template so `symTables` cannot retain state for a + // prospective owner that rollback may erase. + { + SymbolTable newTemplateSymbols(newTemplate); + newTemplateSymbols.insert(newFunc); + } symTables.getSymbolTable(parentModule).insert(newTemplate, Block::iterator(parentTemplate)); if (failed(applyBodyConversions(op, newFunc, paramNameToConcrete))) { std::string newFuncName = newFunc.getSymName().str(); @@ -1681,12 +1952,16 @@ class InstantiateFuncAtCallOp final : public OpRewritePattern { llvm::dbgs() << "[InstantiateFuncAtCallOp] body conversion failed for " << newFuncName << '\n' ); - newTemplate->erase(); + // Erase through the parent table so the operation and its published symbol entry roll back + // together. No table for the prospective owner is retained in `symTables`. + symTables.getSymbolTable(parentModule).erase(newTemplate); return rewriter.notifyMatchFailure(op, [&](Diagnostic &diag) { diag.append("failure while creating instantiated function '", newFuncName, '\''); }); } + ::reportDelayedDiagnostics(op, std::move(deferredExprDiagnostics)); + // Use the post-insertion names. The preferred template name may have collided. SmallVector symPieces = getPieces(op.getCalleeAttr()); assert(symPieces.size() >= 2 && "callee must include at least template and function names"); @@ -2366,10 +2641,10 @@ class UpdateFuncTypeFromReturn final : public OpRewritePattern { } }; -/// Update CallOp result type based on the updated return type from the target FuncDefOp. -/// This only applies to free (i.e., non-struct) functions because the functions within structs -/// only return StructType or nothing and propagating those can result in bringing un-instantiated -/// types from a templated struct into the current call which will give errors. +/// Update a free-function call's result types from its target definition while preserving ordered +/// explicit template arguments. Struct methods are excluded: they return a `StructType` or no +/// value, and copying a method's declaration type back to its call could reintroduce a +/// still-parameterized type after caller-side instantiation. class UpdateFreeFuncCallOpTypes final : public OpRewritePattern { ConversionTracker &tracker_; @@ -2402,7 +2677,11 @@ class UpdateFreeFuncCallOpTypes final : public OpRewritePattern { } LLVM_DEBUG(llvm::dbgs() << "[UpdateFreeFuncCallOpTypes] replaced " << op); - CallOp newOp = replaceOpWithNewOp(rewriter, op, targetFunc, op.getArgOperands()); + ArrayAttr templateParamsAttr = op.getTemplateParamsAttr(); + ArrayRef templateParams = + templateParamsAttr ? templateParamsAttr.getValue() : ArrayRef(); + CallOp newOp = + replaceOpWithNewOp(rewriter, op, targetFunc, op.getArgOperands(), templateParams); (void)newOp; // tell compiler it's intentionally unused in release builds LLVM_DEBUG(llvm::dbgs() << " with " << newOp << '\n'); return success(); @@ -2714,7 +2993,7 @@ class PassImpl : public llzk::polymorphic::impl::FlatteningPassBase { }); } while (tracker.isModified()); - tracker.clearPartialFuncInstantiations(); + tracker.clearFuncInstantiations(); // Run user-selected cleanup first. if (failed(cleanupSwitch(modOp, tracker))) { diff --git a/lib/Dialect/Polymorphic/Transforms/SharedImpl.h b/lib/Dialect/Polymorphic/Transforms/SharedImpl.h index 5961e5e9a4..a526489637 100644 --- a/lib/Dialect/Polymorphic/Transforms/SharedImpl.h +++ b/lib/Dialect/Polymorphic/Transforms/SharedImpl.h @@ -135,7 +135,7 @@ inline component::StructType getStructTypeWithParams( /// a new instantiated template and how to rewrite the remaining argument list at the use site. struct InstantiationLayout { mlir::SmallVector remainingNames; - /// Ordered [parameter-name, concrete-value, ...] entries for exact partial-function reuse. + /// Ordered [parameter-name, concrete-value, ...] entries for exact function-specialization reuse. mlir::ArrayAttr concreteParamKey; std::string templateNameWithAttrs; mlir::ArrayAttr rewrittenCallParams; diff --git a/lib/Dialect/Struct/IR/Types.cpp b/lib/Dialect/Struct/IR/Types.cpp index fced7e1398..5cfadab9dd 100644 --- a/lib/Dialect/Struct/IR/Types.cpp +++ b/lib/Dialect/Struct/IR/Types.cpp @@ -11,6 +11,7 @@ #include "llzk/Dialect/Polymorphic/IR/Ops.h" #include "llzk/Dialect/Struct/IR/Ops.h" +#include "llzk/Util/TypeHelper.h" using namespace mlir; using namespace llzk::polymorphic; @@ -44,15 +45,27 @@ FailureOr> StructType::getDefinition( // If this StructType contains parameters, make sure the StructDefOp is within a TemplateOp with // the same number of params. if (typeParams) { - size_t numExpected = 0; - if (TemplateOp parent = getParentOfType(*res.value())) { - numExpected = parent.numConstOps(); - } + TemplateOp parent = getParentOfType(*res.value()); + size_t numExpected = parent ? parent.numConstOps() : 0; if (typeParams.size() != numExpected) { return op->emitError() << '\'' << StructType::name << "' type has " << typeParams.size() << " parameters but \"" << res.value().get().getSymName() << "\" expects " << numExpected; } + if (parent) { + for (auto [paramOp, value] : + llvm::zip_equal(parent.getConstOps(), typeParams.getValue())) { + std::optional restriction = paramOp.getTypeOpt(); + if (!restriction || llvm::isa(value)) { + continue; + } + if (failed(materializeTemplateParamValue(value, restriction))) { + return op->emitError() << "instantiation value '" << value + << "' is not compatible with parameter \"@" << paramOp.getName() + << "\" type restriction " << *restriction; + } + } + } } return res; } diff --git a/lib/Dialect/Verif/IR/Ops.cpp b/lib/Dialect/Verif/IR/Ops.cpp index 2765d4bdb4..44b7a968c8 100644 --- a/lib/Dialect/Verif/IR/Ops.cpp +++ b/lib/Dialect/Verif/IR/Ops.cpp @@ -12,8 +12,7 @@ #include "llzk/Analysis/AnalysisUtil.h" #include "llzk/Analysis/ConstraintDependencyGraph.h" #include "llzk/Analysis/SourceRef.h" -#include "llzk/Dialect/Felt/IR/Attrs.h" -#include "llzk/Dialect/Felt/IR/Types.h" +#include "llzk/Dialect/Global/IR/Ops.h" #include "llzk/Dialect/LLZK/IR/Ops.h" #include "llzk/Dialect/Polymorphic/IR/Ops.h" #include "llzk/Dialect/Verif/Util/ForbiddenPreconditionInfluence.h" @@ -22,6 +21,7 @@ #include "llzk/Util/ErrorHelper.h" #include "llzk/Util/SymbolHelper.h" #include "llzk/Util/SymbolTableLLZK.h" +#include "llzk/Util/TypeHelper.h" #include "llzk/Util/Walk.h" #include @@ -52,7 +52,6 @@ using namespace mlir; using namespace llzk::polymorphic; -using namespace llzk::felt; using namespace llzk::component; using namespace llzk::function; @@ -781,19 +780,31 @@ LogicalResult IncludeOp::verifyTemplateParamCompatibility( if (std::optional declaredType = targetParam.getTypeOpt()) { // Note: `declaredType` is restricted by `isValidConstReadType()` bool compatible = false; - if (llvm::isa(*declaredType)) { - compatible = llvm::isa(paramFromIncludeOp); - } else if (llvm::isa(*declaredType)) { - compatible = llvm::isa(paramFromIncludeOp) && - isValidConstReadType(llvm::cast(paramFromIncludeOp).getType()); - } else if (llvm::isa(*declaredType)) { - // Note: Just like struct type instantiation, there is no restriction on passing a - // larger value to an `i1`. The flattening pass will treat 0 as false and any other - // value as true (but give a warning if it's not 1). - compatible = llvm::isa(paramFromIncludeOp) && - isValidConstReadType(llvm::cast(paramFromIncludeOp).getType()); + if (auto sym = llvm::dyn_cast(paramFromIncludeOp)) { + bool resolvedLocal = false; + if (sym.getNestedReferences().empty()) { + SymbolTableCollection tables; + FailureOr parentTemplate = getConstResolutionTemplate(tables, *this); + if (failed(parentTemplate)) { + return failure(); + } + if (TemplateOp p = *parentTemplate) { + auto binding = p.getConstNamed(sym.getRootReference()); + if (binding) { + resolvedLocal = true; + compatible = isTemplateParamTypeCompatible(binding.getTypeOpt(), *declaredType); + } + } + } + if (!resolvedLocal) { + SymbolTableCollection tables; + if (auto global = lookupTopLevelSymbol(tables, sym, *this, false); + succeeded(global)) { + compatible = isTemplateParamTypeCompatible(global->get().getType(), *declaredType); + } + } } else { - llvm_unreachable("inconsistent with `isValidConstReadType()`"); + compatible = succeeded(materializeTemplateParamValue(paramFromIncludeOp, declaredType)); } if (!compatible) { return this->emitOpError().append( @@ -825,6 +836,29 @@ LogicalResult IncludeOp::verifyTemplateParamsMatchInferred( const UnificationMap &unifications ) { ArrayAttr callParams = this->getTemplateParamsAttr(); + if (isNullOrEmpty(callParams)) { + for (TemplateParamOp paramOp : targetParamDefs) { + if (std::optional declaredType = paramOp.getTypeOpt(); + declaredType && llvm::isa(*declaredType)) { + continue; + } + auto it = unifications.find({FlatSymbolRefAttr::get(paramOp.getNameAttr()), Side::RHS}); + if (it == unifications.end()) { + // No inferred value means the signature did not expose this parameter to this include. + continue; + } + if (!it->second) { + return this->emitOpError().append( + "cannot infer template instantiation value for parameter \"@", paramOp.getName(), + "\" from contract type signature" + ); + } + if (failed(verifyTemplateParamCompatibility(it->second, paramOp))) { + return failure(); + } + } + return success(); + } assert(!isNullOrEmpty(callParams) && "pre-condition"); assert((callParams.size() == llvm::range_size(targetParamDefs)) && "pre-condition"); @@ -836,10 +870,29 @@ LogicalResult IncludeOp::verifyTemplateParamsMatchInferred( } } auto it = unifications.find({FlatSymbolRefAttr::get(paramOp.getNameAttr()), Side::RHS}); - if (it != unifications.end() && !typeParamsUnify({attr}, {it->second})) { + if (it != unifications.end() && !it->second) { + return this->emitOpError().append( + "cannot infer a unique template instantiation value for parameter \"@", paramOp.getName(), + "\" from contract type signature" + ); + } + if (it != unifications.end() && failed(verifyTemplateParamCompatibility(it->second, paramOp))) { + return failure(); + } + bool valuesUnify = true; + if (it != unifications.end()) { + SymbolTableCollection tables; + FailureOr resolved = + resolvedTemplateParamValuesUnify(tables, *this, attr, it->second, paramOp.getTypeOpt()); + if (failed(resolved)) { + return failure(); + } + valuesUnify = *resolved; + } + if (!valuesUnify) { return this->emitOpError().append( "template instantiation value '", attr, "' for parameter \"@", paramOp.getName(), - "\" conflicts with value '", it->second, "' inferred from function type signature" + "\" conflicts with value '", it->second, "' inferred from contract type signature" ); } } @@ -893,18 +946,18 @@ struct KnownTargetVerifier : public IncludeOpVerifier { LogicalResult verifyTemplateParams() override { Operation *tgtOp = tgt.getOperation(); if (TemplateOp tgtOpParent = getParentOfType(tgtOp)) { - // When the target function is a free function within a TemplateOp, the IncludeOp may have + // When the target contract is within a TemplateOp, the IncludeOp may have // template parameter instantiations that must be checked against the template parameters. - // - If the function type signature references all template parameters, then the parameter + // - If the contract signature references all template parameters, then the parameter // instantiation list on the IncludeOp is optional, otherwise it's required. // - If present, the instantiation list must provide a value for every template parameter // and the value must be type-compatible with the parameter's declared type (if any). - // - If present, the instantiation list must result in a function type signature that can - // be unified with the IncludeOp's operand and result types. + // - If present, the instantiation list must result in a contract signature that can be + // unified with the IncludeOp's operand types. auto realParams = tgtOpParent.getConstOps(); ArrayAttr callParams = includeOp->getTemplateParamsAttr(); - // When there is no instantiation list, just ensure that it's not required. + // When every parameter appears in the signature, infer and validate omitted arguments. if (isNullOrEmpty(callParams)) { llvm::SmallDenseSet referencedInSignature; llzk::getSymbolsUsedIn(tgtType.getInputs(), referencedInSignature); @@ -914,7 +967,11 @@ struct KnownTargetVerifier : public IncludeOpVerifier { return referencedInSignature.contains(FlatSymbolRefAttr::get(p.getNameAttr())); }); if (allParamsReferenced) { - return success(); + FailureOr unifyResult = includeOp->unifyTypeSignature(tgtType); + if (failed(unifyResult)) { + return failure(); + } + return includeOp->verifyTemplateParamsMatchInferred(realParams, unifyResult.value()); } return includeOp->emitOpError().append( "must provide template instantiation parameters when calling \"@", tgt.getSymName(), @@ -945,7 +1002,7 @@ struct KnownTargetVerifier : public IncludeOpVerifier { } // Check that the provided instantiation values are consistent with what type unification - // of the target function types against the call's operand and result types would determine. + // of the target contract signature against the IncludeOp's operand types would determine. FailureOr unifyResult = includeOp->unifyTypeSignature(tgtType); // This is already checked by `verifyInputs()`, but `verifyTemplateParams()` is called // even if `verifyInputs()` fails for error aggregation, so we still need to return @@ -955,7 +1012,7 @@ struct KnownTargetVerifier : public IncludeOpVerifier { } return includeOp->verifyTemplateParamsMatchInferred(realParams, unifyResult.value()); } else { - // Non-template functions cannot contain template parameter instantiations. + // Contracts outside templates cannot have template parameter instantiations. return verifyNoTemplateInstantiations(); } } @@ -1016,7 +1073,7 @@ LogicalResult IncludeOp::verifySymbolUses(SymbolTableCollection &tables) { } // Otherwise, callee must be specified via full path from the root module. Perform the full set of - // checks against the known target function. + // checks against the known target contract. auto tgtOpt = lookupTopLevelSymbol( tables, calleeAttr, getParentOfType(getOperation()) ); diff --git a/lib/Util/SymbolHelper.cpp b/lib/Util/SymbolHelper.cpp index 2a3b659812..63d49334e3 100644 --- a/lib/Util/SymbolHelper.cpp +++ b/lib/Util/SymbolHelper.cpp @@ -15,12 +15,14 @@ #include "llzk/Util/SymbolHelper.h" #include "llzk/Dialect/Array/IR/Ops.h" +#include "llzk/Dialect/Felt/IR/Types.h" #include "llzk/Dialect/Function/IR/Ops.h" #include "llzk/Dialect/Global/IR/Ops.h" #include "llzk/Dialect/Polymorphic/IR/Types.h" #include "llzk/Dialect/Verif/IR/Ops.h" #include "llzk/Util/SymbolLookup.h" #include "llzk/Util/SymbolTableLLZK.h" +#include "llzk/Util/TypeHelper.h" #include #include @@ -216,13 +218,13 @@ LogicalResult verifyTemplateSymbolType( ) { if (requiredParamType) { std::optional actualType = binding.getTypeOpt(); - if (!actualType) { - return origin->emitError().append( - "ref \"", param, "\" in type ", parameterizedType, " refers to a '", binding->getName(), - "' that must have type ", *requiredParamType - ); - } - if (*actualType != *requiredParamType) { + if (!isTemplateParamTypeCompatible(actualType, *requiredParamType)) { + if (!actualType) { + return origin->emitError().append( + "ref \"", param, "\" in type ", parameterizedType, " refers to a '", binding->getName(), + "' that must have type ", *requiredParamType + ); + } return origin->emitError().append( "ref \"", param, "\" in type ", parameterizedType, " refers to a '", binding->getName(), "' with type ", *actualType, " but expected ", *requiredParamType @@ -413,11 +415,17 @@ LogicalResult verifyParamOfType( return failure(); // lookupTopLevelSymbol() already emits a sufficient error message } Operation *foundOp = lookupRes->get(); - if (!llvm::isa(foundOp)) { + auto global = llvm::dyn_cast(foundOp); + if (!global) { return origin->emitError() << "ref \"" << param << "\" in type " << parameterizedType << " refers to a '" << foundOp->getName() << "' which is not allowed"; } + if (requiredParamType && !isTemplateParamTypeCompatible(global.getType(), *requiredParamType)) { + return origin->emitError() << "ref \"" << param << "\" in type " << parameterizedType + << " refers to a global with type " << global.getType() + << " but expected type " << *requiredParamType; + } return success(); } @@ -456,6 +464,145 @@ LogicalResult verifyParamsOfType( return paramCheckResult; } +namespace { + +/// Type and value facts established by resolving one symbolic template argument. +struct TemplateParamSymbolEvidence { + std::optional restriction; + Attribute concreteValue; +}; + +/// Resolve a local template binding or qualified global without rejecting genuinely unknown refs. +FailureOr> resolveTemplateParamSymbolEvidence( + SymbolTableCollection &tables, Operation *origin, SymbolRefAttr symbol +) { + if (symbol.getNestedReferences().empty()) { + FailureOr parent = getConstResolutionTemplate(tables, origin); + if (failed(parent)) { + return failure(); + } + if (*parent) { + auto binding = + parent->getConstNamed(symbol.getRootReference()); + if (binding) { + return std::make_optional(TemplateParamSymbolEvidence {binding.getTypeOpt(), Attribute()}); + } + } + } + + auto global = lookupTopLevelSymbol(tables, symbol, origin, false); + if (succeeded(global)) { + GlobalDefOp globalOp = global->get(); + return std::make_optional( + TemplateParamSymbolEvidence { + globalOp.getType(), + globalOp.getInitialValueAttr(), + } + ); + } + return std::optional(); +} + +/// Return whether two known felt restrictions require different explicit fields. +bool feltRestrictionsConflict(std::optional lhs, std::optional rhs) { + if (!lhs || !rhs) { + return false; + } + auto lhsFelt = llvm::dyn_cast(*lhs); + auto rhsFelt = llvm::dyn_cast(*rhs); + return lhsFelt && rhsFelt && lhsFelt.hasField() && rhsFelt.hasField() && lhsFelt != rhsFelt; +} + +} // namespace + +FailureOr resolvedTemplateParamValuesUnify( + SymbolTableCollection &tables, Operation *origin, Attribute explicitValue, + Attribute inferredValue, std::optional requiredParamType +) { + bool contextFreeResult = + templateParamValuesUnify(explicitValue, inferredValue, requiredParamType); + if (!requiredParamType || !llvm::isa(*requiredParamType)) { + return contextFreeResult; + } + + SymbolRefAttr explicitSymbol = llvm::dyn_cast(explicitValue); + SymbolRefAttr inferredSymbol = llvm::dyn_cast(inferredValue); + if (!explicitSymbol && !inferredSymbol) { + return contextFreeResult; + } + + std::optional explicitEvidence; + std::optional inferredEvidence; + if (explicitSymbol) { + FailureOr> resolved = + resolveTemplateParamSymbolEvidence(tables, origin, explicitSymbol); + if (failed(resolved)) { + return failure(); + } + explicitEvidence = *resolved; + } + if (inferredSymbol) { + FailureOr> resolved = + resolveTemplateParamSymbolEvidence(tables, origin, inferredSymbol); + if (failed(resolved)) { + return failure(); + } + inferredEvidence = *resolved; + } + + // Unresolved references retain the generic unifier's deferral rule. + if ((explicitSymbol && !explicitEvidence) || (inferredSymbol && !inferredEvidence)) { + return contextFreeResult; + } + if (explicitEvidence && inferredEvidence && + feltRestrictionsConflict(explicitEvidence->restriction, inferredEvidence->restriction)) { + return false; + } + + // Replace a resolved global with its value; local bindings retain only their type evidence. + auto materializeEvidence = []( + Attribute fallback, SymbolRefAttr symbol, + const std::optional &evidence + ) -> FailureOr> { + if (!symbol) { + return std::make_optional(fallback); + } + if (!evidence || !evidence->concreteValue) { + return std::optional(); + } + FailureOr materialized = + materializeTemplateParamValue(evidence->concreteValue, evidence->restriction); + if (failed(materialized)) { + return failure(); + } + return std::make_optional(*materialized); + }; + + FailureOr> explicitConcrete = + materializeEvidence(explicitValue, explicitSymbol, explicitEvidence); + FailureOr> inferredConcrete = + materializeEvidence(inferredValue, inferredSymbol, inferredEvidence); + if (failed(explicitConcrete) || failed(inferredConcrete)) { + return false; + } + if (*explicitConcrete && *inferredConcrete) { + return templateParamValuesUnify( + explicitConcrete->value(), inferredConcrete->value(), requiredParamType + ); + } + if (*explicitConcrete && inferredEvidence && inferredEvidence->restriction) { + return succeeded( + materializeTemplateParamValue(explicitConcrete->value(), inferredEvidence->restriction) + ); + } + if (*inferredConcrete && explicitEvidence && explicitEvidence->restriction) { + return succeeded( + materializeTemplateParamValue(inferredConcrete->value(), explicitEvidence->restriction) + ); + } + return contextFreeResult; +} + FailureOr verifyStructTypeResolution(SymbolTableCollection &tables, StructType ty, Operation *origin) { auto res = ty.getDefinition(tables, origin); @@ -477,6 +624,17 @@ verifyStructTypeResolution(SymbolTableCollection &tables, StructType ty, Operati if (failed(verifyParamsOfType(tables, tyParams.getValue(), ty, origin))) { return failure(); // verifyParamsOfType() already emits a sufficient error message } + if (TemplateOp parent = getParentOfType(defForType.getOperation())) { + for (auto [paramOp, value] : + llvm::zip_equal(parent.getConstOps(), tyParams.getValue())) { + std::optional restriction = paramOp.getTypeOpt(); + if (auto symbolValue = llvm::dyn_cast(value); + symbolValue && restriction && + failed(verifyParamOfType(tables, symbolValue, ty, origin, restriction))) { + return failure(); + } + } + } } return defForType; } diff --git a/lib/Util/TypeHelper.cpp b/lib/Util/TypeHelper.cpp index d3515f5250..d584ad3854 100644 --- a/lib/Util/TypeHelper.cpp +++ b/lib/Util/TypeHelper.cpp @@ -929,6 +929,133 @@ bool typesUnify( return UnifierImpl(unifications, rhsReversePrefix).typesUnify(lhs, rhs); } +bool isTemplateParamTypeCompatible(Type actualType, Type requiredType) { + // TypeVarType is a template-argument kind restriction, not an ordinary type wildcard. Keep + // that distinction here rather than weakening typesUnify(), whose type-variable behavior is + // required for general type inference. + bool actualIsTypeOnly = isa(actualType); + bool requiredIsTypeOnly = isa(requiredType); + if (actualIsTypeOnly || requiredIsTypeOnly) { + return actualIsTypeOnly && requiredIsTypeOnly; + } + + FeltType requiredFelt = dyn_cast(requiredType); + if (requiredFelt) { + FeltType actualFelt = dyn_cast(actualType); + if (!actualFelt) { + return false; + } + if (!requiredFelt.hasField()) { + return true; + } + return actualFelt.hasField() && actualFelt == requiredFelt; + } + return typesUnify(actualType, requiredType); +} + +bool isTemplateParamTypeCompatible(std::optional actualType, Type requiredType) { + if (!actualType) { + if (isa(requiredType)) { + return false; + } + if (FeltType requiredFelt = dyn_cast(requiredType)) { + return !requiredFelt.hasField(); + } + return true; + } + return isTemplateParamTypeCompatible(*actualType, requiredType); +} + +FailureOr +materializeTemplateParamValue(Attribute actualValue, std::optional requiredType) { + if (!requiredType) { + return actualValue; + } + + Type restriction = *requiredType; + if (isa(restriction)) { + if (isa(actualValue)) { + return actualValue; + } + return failure(); + } + + if (FeltType feltType = dyn_cast(restriction)) { + if (FeltConstAttr feltValue = dyn_cast(actualValue)) { + FailureOr materialized = feltValue.materializeAs(feltType); + if (failed(materialized)) { + return failure(); + } + return *materialized; + } + if (IntegerAttr integerValue = dyn_cast(actualValue)) { + if (!isValidConstReadType(integerValue.getType())) { + return failure(); + } + return FeltConstAttr::get(actualValue.getContext(), integerValue.getValue(), feltType); + } + return failure(); + } + + if (isa(restriction)) { + if (IntegerAttr integerValue = dyn_cast(actualValue)) { + if (isValidConstReadType(integerValue.getType())) { + return actualValue; + } + return failure(); + } + if (AffineMapAttr affineValue = dyn_cast(actualValue)) { + if (affineValue.getValue().getNumResults() == 1) { + return actualValue; + } + return failure(); + } + } + + return failure(); +} + +bool templateParamValuesUnify( + Attribute actualValue, Attribute inferredValue, std::optional requiredType +) { + FeltType requiredFelt; + if (requiredType) { + requiredFelt = dyn_cast(*requiredType); + } + if (!requiredFelt) { + return typeParamsUnify({actualValue}, {inferredValue}); + } + + auto asFeltConst = [requiredFelt](Attribute value) -> FeltConstAttr { + if (auto feltValue = dyn_cast(value)) { + FailureOr materialized = feltValue.materializeAs(requiredFelt); + return succeeded(materialized) ? *materialized : FeltConstAttr(); + } + if (auto intValue = dyn_cast(value)) { + return FeltConstAttr::get(value.getContext(), intValue.getValue(), requiredFelt); + } + return FeltConstAttr(); + }; + + FeltConstAttr actualFelt = asFeltConst(actualValue); + FeltConstAttr inferredFelt = asFeltConst(inferredValue); + if (!actualFelt || !inferredFelt) { + if ((!actualFelt && !isa(actualValue)) || + (!inferredFelt && !isa(inferredValue))) { + return false; + } + return typeParamsUnify({actualValue}, {inferredValue}); + } + + if (!llvm::APInt::isSameValue(actualFelt.getValue(), inferredFelt.getValue())) { + return false; + } + FeltType actualFeltType = actualFelt.getType(); + FeltType inferredFeltType = inferredFelt.getType(); + return !actualFeltType.hasField() || !inferredFeltType.hasField() || + actualFeltType == inferredFeltType; +} + bool isMoreConcreteUnification( Type oldTy, Type newTy, llvm::function_ref knownOldToNew ) { diff --git a/test/Dialect/Function/call_with_symbolic_felt_reconciliation_fail.llzk b/test/Dialect/Function/call_with_symbolic_felt_reconciliation_fail.llzk new file mode 100644 index 0000000000..d2c35465fc --- /dev/null +++ b/test/Dialect/Function/call_with_symbolic_felt_reconciliation_fail.llzk @@ -0,0 +1,180 @@ +// RUN: llzk-opt -split-input-file -verify-diagnostics %s + +module attributes {llzk.lang} { + poly.template @BoxTemplate { + poly.param @P + struct.def @Box { + function.def @compute() -> !struct.type<@BoxTemplate::@Box<[@P]>> { + %self = struct.new : <@BoxTemplate::@Box<[@P]>> + function.return %self : !struct.type<@BoxTemplate::@Box<[@P]>> + } + function.def @constrain(%self: !struct.type<@BoxTemplate::@Box<[@P]>>) { + function.return + } + } + } + + poly.template @Target { + poly.param @F : !felt.type + function.def @accept(%value: !struct.type<@BoxTemplate::@Box<[@F]>>) { + function.return + } + } + + poly.template @Caller { + poly.param @G : !felt.type<"bn128"> + function.def @field_conflict( + %value: !struct.type<@BoxTemplate::@Box<[#felt>]>> + ) { + // expected-error@+1 {{'function.call' op template instantiation value '@G' for parameter "@F" conflicts with value '#felt> : !felt.type<"goldilocks">' inferred from function type signature}} + function.call @Target::@accept<[@G]>(%value) : + (!struct.type<@BoxTemplate::@Box<[#felt>]>>) -> () + function.return + } + } +} + +// ----- + +module attributes {llzk.lang} { + poly.template @BoxTemplate { + poly.param @P + struct.def @Box { + function.def @compute() -> !struct.type<@BoxTemplate::@Box<[@P]>> { + %self = struct.new : <@BoxTemplate::@Box<[@P]>> + function.return %self : !struct.type<@BoxTemplate::@Box<[@P]>> + } + function.def @constrain(%self: !struct.type<@BoxTemplate::@Box<[@P]>>) { + function.return + } + } + } + + poly.template @Target { + poly.param @F : !felt.type + function.def @accept(%value: !struct.type<@BoxTemplate::@Box<[@F]>>) { + function.return + } + } + + poly.template @Caller { + poly.param @G : !felt.type<"bn128"> + function.def @concrete_vs_inferred_symbol( + %value: !struct.type<@BoxTemplate::@Box<[@G]>> + ) { + // expected-error@+1 {{'function.call' op template instantiation value '#felt> : !felt.type<"goldilocks">' for parameter "@F" conflicts with value '@G' inferred from function type signature}} + function.call @Target::@accept<[#felt>]>(%value) : + (!struct.type<@BoxTemplate::@Box<[@G]>>) -> () + function.return + } + } +} + +// ----- + +module attributes {llzk.lang} { + poly.template @BoxTemplate { + poly.param @P + struct.def @Box { + function.def @compute() -> !struct.type<@BoxTemplate::@Box<[@P]>> { + %self = struct.new : <@BoxTemplate::@Box<[@P]>> + function.return %self : !struct.type<@BoxTemplate::@Box<[@P]>> + } + function.def @constrain(%self: !struct.type<@BoxTemplate::@Box<[@P]>>) { + function.return + } + } + } + + poly.template @Target { + poly.param @F : !felt.type + function.def @accept(%value: !struct.type<@BoxTemplate::@Box<[@F]>>) { + function.return + } + } + + poly.template @Caller { + poly.param @G : !felt.type<"bn128"> + poly.param @H : !felt.type<"goldilocks"> + function.def @symbol_field_conflict(%value: !struct.type<@BoxTemplate::@Box<[@H]>>) { + // expected-error@+1 {{'function.call' op template instantiation value '@G' for parameter "@F" conflicts with value '@H' inferred from function type signature}} + function.call @Target::@accept<[@G]>(%value) : + (!struct.type<@BoxTemplate::@Box<[@H]>>) -> () + function.return + } + } +} + +// ----- + +module attributes {llzk.lang} { + poly.template @BoxTemplate { + poly.param @P + struct.def @Box { + function.def @compute() -> !struct.type<@BoxTemplate::@Box<[@P]>> { + %self = struct.new : <@BoxTemplate::@Box<[@P]>> + function.return %self : !struct.type<@BoxTemplate::@Box<[@P]>> + } + function.def @constrain(%self: !struct.type<@BoxTemplate::@Box<[@P]>>) { + function.return + } + } + } + + poly.template @Target { + poly.param @F : !felt.type + function.def @accept(%value: !struct.type<@BoxTemplate::@Box<[@F]>>) { + function.return + } + } + + module @globals { + global.def const @G : !felt.type<"bn128"> = 35 + } + + function.def @global_field_conflict( + %value: !struct.type<@BoxTemplate::@Box<[#felt>]>> + ) { + // expected-error@+1 {{'function.call' op template instantiation value '@globals::@G' for parameter "@F" conflicts with value '#felt> : !felt.type<"goldilocks">' inferred from function type signature}} + function.call @Target::@accept<[@globals::@G]>(%value) : + (!struct.type<@BoxTemplate::@Box<[#felt>]>>) -> () + function.return + } +} + +// ----- + +module attributes {llzk.lang} { + poly.template @BoxTemplate { + poly.param @P + struct.def @Box { + function.def @compute() -> !struct.type<@BoxTemplate::@Box<[@P]>> { + %self = struct.new : <@BoxTemplate::@Box<[@P]>> + function.return %self : !struct.type<@BoxTemplate::@Box<[@P]>> + } + function.def @constrain(%self: !struct.type<@BoxTemplate::@Box<[@P]>>) { + function.return + } + } + } + + poly.template @Target { + poly.param @F : !felt.type + function.def @accept(%value: !struct.type<@BoxTemplate::@Box<[@F]>>) { + function.return + } + } + + module @globals { + global.def const @G : !felt.type<"bn128"> = 35 + } + + function.def @global_value_conflict( + %value: !struct.type<@BoxTemplate::@Box<[#felt>]>> + ) { + // expected-error@+1 {{'function.call' op template instantiation value '@globals::@G' for parameter "@F" conflicts with value '#felt> : !felt.type<"bn128">' inferred from function type signature}} + function.call @Target::@accept<[@globals::@G]>(%value) : + (!struct.type<@BoxTemplate::@Box<[#felt>]>>) -> () + function.return + } +} diff --git a/test/Dialect/Function/call_with_symbolic_felt_restriction_pass.llzk b/test/Dialect/Function/call_with_symbolic_felt_restriction_pass.llzk new file mode 100644 index 0000000000..67858341bf --- /dev/null +++ b/test/Dialect/Function/call_with_symbolic_felt_restriction_pass.llzk @@ -0,0 +1,90 @@ +// RUN: llzk-opt -split-input-file %s + +// A fielded symbolic argument satisfies a fieldless felt restriction. +module attributes {llzk.lang} { + poly.template @BoxTemplate { + poly.param @P + struct.def @Box { + function.def @compute() -> !struct.type<@BoxTemplate::@Box<[@P]>> { + %self = struct.new : <@BoxTemplate::@Box<[@P]>> + function.return %self : !struct.type<@BoxTemplate::@Box<[@P]>> + } + function.def @constrain(%self: !struct.type<@BoxTemplate::@Box<[@P]>>) { + function.return + } + } + } + + poly.template @Target { + poly.param @F : !felt.type + function.def @accept(%value: !struct.type<@BoxTemplate::@Box<[@F]>>) { + function.return + } + } + + poly.template @Caller { + poly.param @G : !felt.type<"bn128"> + poly.param @H : !felt.type<"bn128"> + function.def @caller(%value: !struct.type<@BoxTemplate::@Box<[@G]>>) { + function.call @Target::@accept<[@G]>(%value) : + (!struct.type<@BoxTemplate::@Box<[@G]>>) -> () + function.call @Target::@accept(%value) : + (!struct.type<@BoxTemplate::@Box<[@G]>>) -> () + function.return + } + function.def @concrete(%value: !struct.type<@BoxTemplate::@Box<[ + #felt> + ]>>) { + function.call @Target::@accept<[@G]>(%value) : + (!struct.type<@BoxTemplate::@Box<[ + #felt> + ]>>) -> () + function.return + } + function.def @fieldless_concrete( + %value: !struct.type<@BoxTemplate::@Box<[#felt]>> + ) { + function.call @Target::@accept<[@G]>(%value) : + (!struct.type<@BoxTemplate::@Box<[#felt]>>) -> () + function.return + } + function.def @concrete_vs_symbol(%value: !struct.type<@BoxTemplate::@Box<[@G]>>) { + function.call @Target::@accept<[#felt>]>(%value) : + (!struct.type<@BoxTemplate::@Box<[@G]>>) -> () + function.return + } + function.def @symbols_match(%value: !struct.type<@BoxTemplate::@Box<[@H]>>) { + function.call @Target::@accept<[@G]>(%value) : + (!struct.type<@BoxTemplate::@Box<[@H]>>) -> () + function.return + } + } + + module @globals { + global.def const @G : !felt.type<"bn128"> = 35 + } + + function.def @qualified(%value: !struct.type<@BoxTemplate::@Box<[@globals::@G]>>) { + function.call @Target::@accept<[@globals::@G]>(%value) : + (!struct.type<@BoxTemplate::@Box<[@globals::@G]>>) -> () + function.return + } + + function.def @qualified_concrete(%value: !struct.type<@BoxTemplate::@Box<[ + #felt> + ]>>) { + function.call @Target::@accept<[@globals::@G]>(%value) : + (!struct.type<@BoxTemplate::@Box<[ + #felt> + ]>>) -> () + function.return + } + + function.def @qualified_fieldless_concrete( + %value: !struct.type<@BoxTemplate::@Box<[#felt]>> + ) { + function.call @Target::@accept<[@globals::@G]>(%value) : + (!struct.type<@BoxTemplate::@Box<[#felt]>>) -> () + function.return + } +} diff --git a/test/Dialect/Function/call_with_template_params_fail.llzk b/test/Dialect/Function/call_with_template_params_fail.llzk index 3b9fed87c0..622944a4eb 100644 --- a/test/Dialect/Function/call_with_template_params_fail.llzk +++ b/test/Dialect/Function/call_with_template_params_fail.llzk @@ -21,6 +21,27 @@ module attributes {llzk.lang} { function.def @constrain(%self: !struct.type<@MissingTemplateParams>) { function.return } } } + +// ----- + +module attributes {llzk.lang} { + poly.template @RepeatedParam { + poly.param @N + + function.def @accept( + %lhs: !array.type<@N x index>, %rhs: !array.type<@N x index> + ) { + function.return + } + } + + function.def @caller(%lhs: !array.type<5 x index>, %rhs: !array.type<7 x index>) { + // expected-error@+1 {{'function.call' op cannot infer a unique template instantiation value for parameter "@N" from function type signature}} + function.call @RepeatedParam::@accept<[5]>(%lhs, %rhs) : + (!array.type<5 x index>, !array.type<7 x index>) -> () + function.return + } +} // ----- module attributes {llzk.lang} { poly.template @GlobalFunc { @@ -238,3 +259,25 @@ module attributes {llzk.lang} { function.def @constrain(%self: !struct.type<@CannotUnifyWithInferredD>) { function.return } } } +// ----- +module attributes {llzk.lang} { + poly.template @GlobalFunc { + poly.param @A : !felt.type<"bn128"> + + function.def @id() -> index { + %value = arith.constant 7 : index + function.return %value : index + } + } + + struct.def @WrongFeltField { + function.def @compute() -> !struct.type<@WrongFeltField> { + %self = struct.new : !struct.type<@WrongFeltField> + // expected-error@+1 {{'function.call' op instantiation value '#felt> : !felt.type<"goldilocks">' is not compatible with parameter "@A" type restriction '!felt.type<"bn128">'}} + %0 = function.call @GlobalFunc::@id<[#felt>]>() : () -> index + function.return %self : !struct.type<@WrongFeltField> + } + + function.def @constrain(%self: !struct.type<@WrongFeltField>) { function.return } + } +} diff --git a/test/Dialect/Struct/struct_params_fail.llzk b/test/Dialect/Struct/struct_params_fail.llzk index ffa999b762..885aa252d8 100644 --- a/test/Dialect/Struct/struct_params_fail.llzk +++ b/test/Dialect/Struct/struct_params_fail.llzk @@ -675,3 +675,108 @@ module attributes {llzk.lang} { } } } + +// ----- + +// A concrete felt value must satisfy the declared field restriction even if the parameter is not +// read by the instantiated body. +// expected-error@+1 {{instantiation value '#felt> : !felt.type<"goldilocks">' is not compatible with parameter "@N" type restriction '!felt.type<"bn128">'}} +module attributes {llzk.lang, llzk.main = !struct.type<@T::@S<[#felt>]>>} { + poly.template @T { + poly.param @N : !felt.type<"bn128"> + + struct.def @S { + function.def @compute() -> !struct.type<@T::@S<[@N]>> { + %self = struct.new : <@T::@S<[@N]>> + function.return %self : !struct.type<@T::@S<[@N]>> + } + + function.def @constrain(%self: !struct.type<@T::@S<[@N]>>) { + function.return + } + } + } +} + +// ----- + +// A TypeAttr cannot instantiate a felt value parameter. +// expected-error@+1 {{is not compatible with parameter "@N" type restriction '!felt.type<"bn128">'}} +module attributes {llzk.lang, llzk.main = !struct.type<@T::@S<[index]>>} { + poly.template @T { + poly.param @N : !felt.type<"bn128"> + + struct.def @S { + function.def @compute() -> !struct.type<@T::@S<[@N]>> { + %self = struct.new : !struct.type<@T::@S<[@N]>> + function.return %self : !struct.type<@T::@S<[@N]>> + } + function.def @constrain(%self: !struct.type<@T::@S<[@N]>>) { function.return } + } + } +} + +// ----- + +// An IntegerAttr cannot instantiate a type-variable parameter. +// expected-error@+1 {{is not compatible with parameter "@Ty" type restriction '!poly.tvar<@Ty>'}} +module attributes {llzk.lang, llzk.main = !struct.type<@T::@S<[35]>>} { + poly.template @T { + poly.param @Ty : !poly.tvar<@Ty> + + struct.def @S { + function.def @compute() -> !struct.type<@T::@S<[@Ty]>> { + %self = struct.new : !struct.type<@T::@S<[@Ty]>> + function.return %self : !struct.type<@T::@S<[@Ty]>> + } + function.def @constrain(%self: !struct.type<@T::@S<[@Ty]>>) { function.return } + } + } +} + +// ----- + +// A TypeAttr cannot instantiate an index parameter. +// expected-error@+1 {{is not compatible with parameter "@N" type restriction 'index'}} +module attributes {llzk.lang, llzk.main = !struct.type<@T::@S<[!felt.type]>>} { + poly.template @T { + poly.param @N : index + + struct.def @S { + function.def @compute() -> !struct.type<@T::@S<[@N]>> { + %self = struct.new : !struct.type<@T::@S<[@N]>> + function.return %self : !struct.type<@T::@S<[@N]>> + } + function.def @constrain(%self: !struct.type<@T::@S<[@N]>>) { function.return } + } + } +} + +// ----- + +#map = affine_map<(i) -> (i)> +// An affine map is deferred only for an integer-like restriction, not a felt restriction. +module attributes {llzk.lang} { + poly.template @T { + poly.param @N : !felt.type<"bn128"> + + struct.def @S { + function.def @compute() -> !struct.type<@T::@S<[@N]>> { + %self = struct.new : !struct.type<@T::@S<[@N]>> + function.return %self : !struct.type<@T::@S<[@N]>> + } + function.def @constrain(%self: !struct.type<@T::@S<[@N]>>) { function.return } + } + } + + struct.def @Use { + // expected-error@+1 {{is not compatible with parameter "@N" type restriction '!felt.type<"bn128">'}} + struct.member @value : !struct.type<@T::@S<[#map]>> + + function.def @compute() -> !struct.type<@Use> { + %self = struct.new : !struct.type<@Use> + function.return %self : !struct.type<@Use> + } + function.def @constrain(%self: !struct.type<@Use>) { function.return } + } +} diff --git a/test/Dialect/Struct/struct_params_restrictions_pass.llzk b/test/Dialect/Struct/struct_params_restrictions_pass.llzk new file mode 100644 index 0000000000..ee44dfa420 --- /dev/null +++ b/test/Dialect/Struct/struct_params_restrictions_pass.llzk @@ -0,0 +1,95 @@ +// RUN: llzk-opt -split-input-file %s + +// A fieldless concrete felt can be materialized in the required field. +module attributes {llzk.lang, llzk.main = !struct.type<@T::@S<[#felt]>>} { + poly.template @T { + poly.param @N : !felt.type<"bn128"> + + struct.def @S { + function.def @compute() -> !struct.type<@T::@S<[@N]>> { + %self = struct.new : !struct.type<@T::@S<[@N]>> + function.return %self : !struct.type<@T::@S<[@N]>> + } + function.def @constrain(%self: !struct.type<@T::@S<[@N]>>) { function.return } + } + } +} + +// ----- + +// A concrete TypeAttr can instantiate a type-only restriction. +module attributes {llzk.lang, llzk.main = !struct.type<@T::@S<[!felt.type]>>} { + poly.template @T { + poly.param @N : !poly.tvar<@N> + + struct.def @S { + function.def @compute() -> !struct.type<@T::@S<[@N]>> { + %self = struct.new : !struct.type<@T::@S<[@N]>> + function.return %self : !struct.type<@T::@S<[@N]>> + } + function.def @constrain(%self: !struct.type<@T::@S<[@N]>>) { function.return } + } + } +} + +// ----- + +// An explicitly fielded concrete felt with the same field is accepted. +module attributes {llzk.lang, llzk.main = !struct.type<@T::@S<[#felt>]>>} { + poly.template @T { + poly.param @N : !felt.type<"bn128"> + + struct.def @S { + function.def @compute() -> !struct.type<@T::@S<[@N]>> { + %self = struct.new : !struct.type<@T::@S<[@N]>> + function.return %self : !struct.type<@T::@S<[@N]>> + } + function.def @constrain(%self: !struct.type<@T::@S<[@N]>>) { function.return } + } + } +} + +// ----- + +// An integer argument can also be materialized in the required field. +module attributes {llzk.lang, llzk.main = !struct.type<@T::@S<[35]>>} { + poly.template @T { + poly.param @N : !felt.type<"bn128"> + + struct.def @S { + function.def @compute() -> !struct.type<@T::@S<[@N]>> { + %self = struct.new : !struct.type<@T::@S<[@N]>> + function.return %self : !struct.type<@T::@S<[@N]>> + } + function.def @constrain(%self: !struct.type<@T::@S<[@N]>>) { function.return } + } + } +} + +// ----- + +// A one-result affine map remains deferred for an integer-like restriction. +#map = affine_map<(i) -> (i)> +module attributes {llzk.lang} { + poly.template @T { + poly.param @N : index + + struct.def @S { + function.def @compute() -> !struct.type<@T::@S<[@N]>> { + %self = struct.new : !struct.type<@T::@S<[@N]>> + function.return %self : !struct.type<@T::@S<[@N]>> + } + function.def @constrain(%self: !struct.type<@T::@S<[@N]>>) { function.return } + } + } + + struct.def @Use { + struct.member @value : !struct.type<@T::@S<[#map]>> + + function.def @compute() -> !struct.type<@Use> { + %self = struct.new : !struct.type<@Use> + function.return %self : !struct.type<@Use> + } + function.def @constrain(%self: !struct.type<@Use>) { function.return } + } +} diff --git a/test/Dialect/Struct/struct_params_symbolic_fail.llzk b/test/Dialect/Struct/struct_params_symbolic_fail.llzk new file mode 100644 index 0000000000..10c1c405c0 --- /dev/null +++ b/test/Dialect/Struct/struct_params_symbolic_fail.llzk @@ -0,0 +1,249 @@ +// RUN: llzk-opt -split-input-file -verify-diagnostics %s + +// A mismatched field is rejected for a local symbolic argument. +module attributes {llzk.lang} { + poly.template @Target { + poly.param @F : !felt.type<"bn128"> + struct.def @S { + function.def @compute() -> !struct.type<@Target::@S<[@F]>> { + %self = struct.new : <@Target::@S<[@F]>> + function.return %self : !struct.type<@Target::@S<[@F]>> + } + function.def @constrain(%self: !struct.type<@Target::@S<[@F]>>) { + function.return + } + } + } + poly.template @Caller { + poly.param @G : !felt.type<"goldilocks"> + // expected-error@+1 {{ref "@G" in type '!struct.type<@Target::@S<[@G]>>' refers to a 'poly.param' with type '!felt.type<"goldilocks">' but expected '!felt.type<"bn128">'}} + function.def @useLocal(%value: !struct.type<@Target::@S<[@G]>>) { + function.return + } + } +} + +// ----- + +// A value-valued qualified global cannot satisfy a type-only restriction. +module attributes {llzk.lang} { + poly.template @Target { + poly.param @T : !poly.tvar<@T> + struct.def @S { + function.def @compute() -> !struct.type<@Target::@S<[@T]>> { + %self = struct.new : <@Target::@S<[@T]>> + function.return %self : !struct.type<@Target::@S<[@T]>> + } + function.def @constrain(%self: !struct.type<@Target::@S<[@T]>>) { + function.return + } + } + } + module @globals { + global.def const @Index : index = 35 + } + // expected-error@+1 {{ref "@globals::@Index" in type '!struct.type<@Target::@S<[@globals::@Index]>>' refers to a global with type 'index' but expected type '!poly.tvar<@T>'}} + function.def @useIndex(%value: !struct.type<@Target::@S<[@globals::@Index]>>) { + function.return + } +} + +// ----- + +// A felt-valued qualified global cannot satisfy a type-only restriction. +module attributes {llzk.lang} { + poly.template @Target { + poly.param @T : !poly.tvar<@T> + struct.def @S { + function.def @compute() -> !struct.type<@Target::@S<[@T]>> { + %self = struct.new : <@Target::@S<[@T]>> + function.return %self : !struct.type<@Target::@S<[@T]>> + } + function.def @constrain(%self: !struct.type<@Target::@S<[@T]>>) { + function.return + } + } + } + module @globals { + global.def const @Felt : !felt.type<"bn128"> = 35 + } + // expected-error@+1 {{ref "@globals::@Felt" in type '!struct.type<@Target::@S<[@globals::@Felt]>>' refers to a global with type '!felt.type<"bn128">' but expected type '!poly.tvar<@T>'}} + function.def @useFelt(%value: !struct.type<@Target::@S<[@globals::@Felt]>>) { + function.return + } +} + +// ----- + +// A value-valued local parameter cannot satisfy a type-only restriction. +module attributes {llzk.lang} { + poly.template @Target { + poly.param @T : !poly.tvar<@T> + struct.def @S { + function.def @compute() -> !struct.type<@Target::@S<[@T]>> { + %self = struct.new : <@Target::@S<[@T]>> + function.return %self : !struct.type<@Target::@S<[@T]>> + } + function.def @constrain(%self: !struct.type<@Target::@S<[@T]>>) { + function.return + } + } + } + poly.template @IndexCaller { + poly.param @N : index + // expected-error@+1 {{ref "@N" in type '!struct.type<@Target::@S<[@N]>>' refers to a 'poly.param' with type 'index' but expected '!poly.tvar<@T>'}} + function.def @useType(%value: !struct.type<@Target::@S<[@N]>>) { + function.return + } + } +} + +// ----- + +// A type-only local parameter cannot satisfy a value-valued index restriction. +module attributes {llzk.lang} { + poly.template @Target { + poly.param @N : index + struct.def @S { + function.def @compute() -> !struct.type<@Target::@S<[@N]>> { + %self = struct.new : <@Target::@S<[@N]>> + function.return %self : !struct.type<@Target::@S<[@N]>> + } + function.def @constrain(%self: !struct.type<@Target::@S<[@N]>>) { + function.return + } + } + } + poly.template @TypeCaller { + poly.param @T : !poly.tvar<@T> + // expected-error@+1 {{ref "@T" in type '!struct.type<@Target::@S<[@T]>>' refers to a 'poly.param' with type '!poly.tvar<@T>' but expected 'index'}} + function.def @useIndex(%value: !struct.type<@Target::@S<[@T]>>) { + function.return + } + } +} + +// ----- + +// An unrestricted local parameter does not establish a type-only argument kind. +module attributes {llzk.lang} { + poly.template @Target { + poly.param @T : !poly.tvar<@T> + struct.def @S { + function.def @compute() -> !struct.type<@Target::@S<[@T]>> { + %self = struct.new : <@Target::@S<[@T]>> + function.return %self : !struct.type<@Target::@S<[@T]>> + } + function.def @constrain(%self: !struct.type<@Target::@S<[@T]>>) { + function.return + } + } + } + poly.template @UnrestrictedCaller { + poly.param @U + // expected-error@+1 {{ref "@U" in type '!struct.type<@Target::@S<[@U]>>' refers to a 'poly.param' that must have type '!poly.tvar<@T>'}} + function.def @useType(%value: !struct.type<@Target::@S<[@U]>>) { + function.return + } + } +} + +// ----- + +// A mismatched field is rejected for a qualified global argument. +module attributes {llzk.lang} { + poly.template @Target { + poly.param @F : !felt.type<"bn128"> + struct.def @S { + function.def @compute() -> !struct.type<@Target::@S<[@F]>> { + %self = struct.new : <@Target::@S<[@F]>> + function.return %self : !struct.type<@Target::@S<[@F]>> + } + function.def @constrain(%self: !struct.type<@Target::@S<[@F]>>) { + function.return + } + } + } + module @globals { + global.def const @G : !felt.type<"goldilocks"> = 35 + } + // expected-error@+1 {{ref "@globals::@G" in type '!struct.type<@Target::@S<[@globals::@G]>>' refers to a global with type '!felt.type<"goldilocks">' but expected type '!felt.type<"bn128">'}} + function.def @useGlobal(%value: !struct.type<@Target::@S<[@globals::@G]>>) { + function.return + } +} + +// ----- + +// A non-felt qualified global is rejected for an index restriction. +module attributes {llzk.lang} { + poly.template @Target { + poly.param @N : index + struct.def @S { + function.def @compute() -> !struct.type<@Target::@S<[@N]>> { + %self = struct.new : <@Target::@S<[@N]>> + function.return %self : !struct.type<@Target::@S<[@N]>> + } + function.def @constrain(%self: !struct.type<@Target::@S<[@N]>>) { + function.return + } + } + } + module @globals { + global.def const @G : !felt.type = 35 + } + // expected-error@+1 {{ref "@globals::@G" in type '!struct.type<@Target::@S<[@globals::@G]>>' refers to a global with type '!felt.type' but expected type 'index'}} + function.def @useGlobal(%value: !struct.type<@Target::@S<[@globals::@G]>>) { + function.return + } +} + +// ----- + +// A fieldless symbolic restriction cannot establish a fielded target restriction. +module attributes {llzk.lang} { + poly.template @Target { + poly.param @F : !felt.type<"bn128"> + struct.def @S { + function.def @compute() -> !struct.type<@Target::@S<[@F]>> { + %self = struct.new : <@Target::@S<[@F]>> + function.return %self : !struct.type<@Target::@S<[@F]>> + } + function.def @constrain(%self: !struct.type<@Target::@S<[@F]>>) { + function.return + } + } + } + poly.template @Caller { + poly.param @G : !felt.type + // expected-error@+1 {{ref "@G" in type '!struct.type<@Target::@S<[@G]>>' refers to a 'poly.param' with type '!felt.type' but expected '!felt.type<"bn128">'}} + function.def @useLocal(%value: !struct.type<@Target::@S<[@G]>>) { + function.return + } + } +} + +// ----- + +// A fieldless qualified global restriction cannot establish a fielded target restriction. +module attributes {llzk.lang} { + poly.template @Target { + poly.param @F : !felt.type<"bn128"> + struct.def @S { + function.def @compute() -> !struct.type<@Target::@S<[@F]>> { + %self = struct.new : <@Target::@S<[@F]>> + function.return %self : !struct.type<@Target::@S<[@F]>> + } + function.def @constrain(%self: !struct.type<@Target::@S<[@F]>>) { + function.return + } + } + } + module @globals { + global.def const @G : !felt.type = 35 + } + // expected-error@+1 {{ref "@globals::@G" in type '!struct.type<@Target::@S<[@globals::@G]>>' refers to a global with type '!felt.type' but expected type '!felt.type<"bn128">'}} + function.def @useGlobal(%value: !struct.type<@Target::@S<[@globals::@G]>>) { + function.return + } +} diff --git a/test/Dialect/Struct/struct_params_symbolic_pass.llzk b/test/Dialect/Struct/struct_params_symbolic_pass.llzk new file mode 100644 index 0000000000..7cd5afdff4 --- /dev/null +++ b/test/Dialect/Struct/struct_params_symbolic_pass.llzk @@ -0,0 +1,90 @@ +// RUN: llzk-opt -split-input-file %s + +// A fielded local symbolic argument satisfies a fieldless struct restriction. +module attributes {llzk.lang} { + poly.template @Target { + poly.param @F : !felt.type + struct.def @S { + function.def @compute() -> !struct.type<@Target::@S<[@F]>> { + %self = struct.new : <@Target::@S<[@F]>> + function.return %self : !struct.type<@Target::@S<[@F]>> + } + function.def @constrain(%self: !struct.type<@Target::@S<[@F]>>) { + function.return + } + } + } + poly.template @Caller { + poly.param @G : !felt.type<"bn128"> + function.def @useLocal(%value: !struct.type<@Target::@S<[@G]>>) { + function.return + } + } +} + +// ----- + +// A fielded qualified global argument satisfies a fieldless struct restriction. +module attributes {llzk.lang} { + poly.template @Target { + poly.param @F : !felt.type + struct.def @S { + function.def @compute() -> !struct.type<@Target::@S<[@F]>> { + %self = struct.new : <@Target::@S<[@F]>> + function.return %self : !struct.type<@Target::@S<[@F]>> + } + function.def @constrain(%self: !struct.type<@Target::@S<[@F]>>) { + function.return + } + } + } + module @globals { + global.def const @G : !felt.type<"bn128"> = 35 + } + function.def @useGlobal(%value: !struct.type<@Target::@S<[@globals::@G]>>) { + function.return + } +} + +// ----- + +// A qualified global with a concrete type satisfies an index restriction, while a local type-only +// parameter satisfies a type-only restriction. +module attributes {llzk.lang} { + poly.template @IndexTarget { + poly.param @N : index + struct.def @S { + function.def @compute() -> !struct.type<@IndexTarget::@S<[@N]>> { + %self = struct.new : <@IndexTarget::@S<[@N]>> + function.return %self : !struct.type<@IndexTarget::@S<[@N]>> + } + function.def @constrain(%self: !struct.type<@IndexTarget::@S<[@N]>>) { + function.return + } + } + } + poly.template @TypeTarget { + poly.param @T : !poly.tvar<@T> + struct.def @S { + function.def @compute() -> !struct.type<@TypeTarget::@S<[@T]>> { + %self = struct.new : <@TypeTarget::@S<[@T]>> + function.return %self : !struct.type<@TypeTarget::@S<[@T]>> + } + function.def @constrain(%self: !struct.type<@TypeTarget::@S<[@T]>>) { + function.return + } + } + } + poly.template @TypeCaller { + poly.param @U : !poly.tvar<@U> + function.def @useLocal(%value: !struct.type<@TypeTarget::@S<[@U]>>) { + function.return + } + } + module @globals { + global.def const @G : index = 35 + } + function.def @useIndex(%value: !struct.type<@IndexTarget::@S<[@globals::@G]>>) { + function.return + } +} diff --git a/test/Dialect/Verif/contracts_fail.llzk b/test/Dialect/Verif/contracts_fail.llzk index 8a7e8d62dd..2d0d8c0186 100644 --- a/test/Dialect/Verif/contracts_fail.llzk +++ b/test/Dialect/Verif/contracts_fail.llzk @@ -14,6 +14,39 @@ module attributes {llzk.lang} { // ----- +module attributes {llzk.lang} { + poly.template @RepeatedParam { + poly.param @N + + function.def @target( + %lhs: !array.type<@N x index>, %rhs: !array.type<@N x index> + ) { + function.return + } + + verif.contract @Base for @RepeatedParam::@target ( + %lhs: !array.type<@N x index>, %rhs: !array.type<@N x index> + ) { + %ok = arith.constant true + verif.ensure_compute %ok + } + } + + function.def @caller(%lhs: !array.type<5 x index>, %rhs: !array.type<7 x index>) { + function.return + } + + verif.contract @Wrapper for @caller ( + %lhs: !array.type<5 x index>, %rhs: !array.type<7 x index> + ) { + // expected-error@+1 {{'verif.include' op cannot infer a unique template instantiation value for parameter "@N" from contract type signature}} + verif.include @RepeatedParam::@Base<[5]>(%lhs, %rhs) : + (!array.type<5 x index>, !array.type<7 x index>) -> () + } +} + +// ----- + module attributes {llzk.lang} { function.def @check(%x: index, %y: index) { function.return @@ -1230,3 +1263,23 @@ module attributes {llzk.lang} { } } } + +// ----- + +module attributes {llzk.lang} { + poly.template @SpecTemplate { + poly.param @N : !felt.type<"bn128"> + + function.def @token() { function.return } + + verif.contract @Base for @SpecTemplate::@token () { + %ok = arith.constant true + verif.ensure_compute %ok + } + + verif.contract @Wrapper for @SpecTemplate::@token () { + // expected-error@+1 {{'verif.include' op instantiation value '#felt> : !felt.type<"goldilocks">' is not compatible with parameter "@N" type restriction '!felt.type<"bn128">'}} + verif.include @SpecTemplate::@Base<[#felt>]>() : () -> () + } + } +} diff --git a/test/Dialect/Verif/include_with_symbolic_felt_reconciliation_fail.llzk b/test/Dialect/Verif/include_with_symbolic_felt_reconciliation_fail.llzk new file mode 100644 index 0000000000..9f943d6088 --- /dev/null +++ b/test/Dialect/Verif/include_with_symbolic_felt_reconciliation_fail.llzk @@ -0,0 +1,230 @@ +// RUN: llzk-opt -split-input-file -verify-diagnostics %s + +module attributes {llzk.lang} { + poly.template @BoxTemplate { + poly.param @P + struct.def @Box { + function.def @compute() -> !struct.type<@BoxTemplate::@Box<[@P]>> { + %self = struct.new : <@BoxTemplate::@Box<[@P]>> + function.return %self : !struct.type<@BoxTemplate::@Box<[@P]>> + } + function.def @constrain(%self: !struct.type<@BoxTemplate::@Box<[@P]>>) { + function.return + } + } + } + + poly.template @Target { + poly.param @F : !felt.type + function.def @accept(%value: !struct.type<@BoxTemplate::@Box<[@F]>>) { + function.return + } + verif.contract @Base for @Target::@accept ( + %value: !struct.type<@BoxTemplate::@Box<[@F]>> + ) { + %ok = arith.constant true + verif.ensure_compute %ok + } + } + + poly.template @Caller { + poly.param @G : !felt.type<"bn128"> + function.def @field_conflict( + %value: !struct.type<@BoxTemplate::@Box<[#felt>]>> + ) { + function.return + } + verif.contract @LocalFieldConflict for @Caller::@field_conflict ( + %value: !struct.type<@BoxTemplate::@Box<[#felt>]>> + ) { + // expected-error@+1 {{'verif.include' op template instantiation value '@G' for parameter "@F" conflicts with value '#felt> : !felt.type<"goldilocks">' inferred from contract type signature}} + verif.include @Target::@Base<[@G]>(%value) : + (!struct.type<@BoxTemplate::@Box<[#felt>]>>) -> () + } + } +} + +// ----- + +module attributes {llzk.lang} { + poly.template @BoxTemplate { + poly.param @P + struct.def @Box { + function.def @compute() -> !struct.type<@BoxTemplate::@Box<[@P]>> { + %self = struct.new : <@BoxTemplate::@Box<[@P]>> + function.return %self : !struct.type<@BoxTemplate::@Box<[@P]>> + } + function.def @constrain(%self: !struct.type<@BoxTemplate::@Box<[@P]>>) { + function.return + } + } + } + + poly.template @Target { + poly.param @F : !felt.type + function.def @accept(%value: !struct.type<@BoxTemplate::@Box<[@F]>>) { + function.return + } + verif.contract @Base for @Target::@accept ( + %value: !struct.type<@BoxTemplate::@Box<[@F]>> + ) { + %ok = arith.constant true + verif.ensure_compute %ok + } + } + + poly.template @Caller { + poly.param @G : !felt.type<"bn128"> + function.def @concrete_vs_inferred_symbol( + %value: !struct.type<@BoxTemplate::@Box<[@G]>> + ) { + function.return + } + verif.contract @ConcreteVsInferredSymbol for @Caller::@concrete_vs_inferred_symbol ( + %value: !struct.type<@BoxTemplate::@Box<[@G]>> + ) { + // expected-error@+1 {{'verif.include' op template instantiation value '#felt> : !felt.type<"goldilocks">' for parameter "@F" conflicts with value '@G' inferred from contract type signature}} + verif.include @Target::@Base<[#felt>]>(%value) : + (!struct.type<@BoxTemplate::@Box<[@G]>>) -> () + } + } +} + +// ----- + +module attributes {llzk.lang} { + poly.template @BoxTemplate { + poly.param @P + struct.def @Box { + function.def @compute() -> !struct.type<@BoxTemplate::@Box<[@P]>> { + %self = struct.new : <@BoxTemplate::@Box<[@P]>> + function.return %self : !struct.type<@BoxTemplate::@Box<[@P]>> + } + function.def @constrain(%self: !struct.type<@BoxTemplate::@Box<[@P]>>) { + function.return + } + } + } + + poly.template @Target { + poly.param @F : !felt.type + function.def @accept(%value: !struct.type<@BoxTemplate::@Box<[@F]>>) { + function.return + } + verif.contract @Base for @Target::@accept ( + %value: !struct.type<@BoxTemplate::@Box<[@F]>> + ) { + %ok = arith.constant true + verif.ensure_compute %ok + } + } + + poly.template @Caller { + poly.param @G : !felt.type<"bn128"> + poly.param @H : !felt.type<"goldilocks"> + function.def @symbol_field_conflict(%value: !struct.type<@BoxTemplate::@Box<[@H]>>) { + function.return + } + verif.contract @SymbolFieldConflict for @Caller::@symbol_field_conflict ( + %value: !struct.type<@BoxTemplate::@Box<[@H]>> + ) { + // expected-error@+1 {{'verif.include' op template instantiation value '@G' for parameter "@F" conflicts with value '@H' inferred from contract type signature}} + verif.include @Target::@Base<[@G]>(%value) : + (!struct.type<@BoxTemplate::@Box<[@H]>>) -> () + } + } +} + +// ----- + +module attributes {llzk.lang} { + poly.template @BoxTemplate { + poly.param @P + struct.def @Box { + function.def @compute() -> !struct.type<@BoxTemplate::@Box<[@P]>> { + %self = struct.new : <@BoxTemplate::@Box<[@P]>> + function.return %self : !struct.type<@BoxTemplate::@Box<[@P]>> + } + function.def @constrain(%self: !struct.type<@BoxTemplate::@Box<[@P]>>) { + function.return + } + } + } + + poly.template @Target { + poly.param @F : !felt.type + function.def @accept(%value: !struct.type<@BoxTemplate::@Box<[@F]>>) { + function.return + } + verif.contract @Base for @Target::@accept ( + %value: !struct.type<@BoxTemplate::@Box<[@F]>> + ) { + %ok = arith.constant true + verif.ensure_compute %ok + } + } + + module @globals { + global.def const @G : !felt.type<"bn128"> = 35 + } + + function.def @global_field_conflict( + %value: !struct.type<@BoxTemplate::@Box<[#felt>]>> + ) { + function.return + } + verif.contract @GlobalFieldConflict for @global_field_conflict ( + %value: !struct.type<@BoxTemplate::@Box<[#felt>]>> + ) { + // expected-error@+1 {{'verif.include' op template instantiation value '@globals::@G' for parameter "@F" conflicts with value '#felt> : !felt.type<"goldilocks">' inferred from contract type signature}} + verif.include @Target::@Base<[@globals::@G]>(%value) : + (!struct.type<@BoxTemplate::@Box<[#felt>]>>) -> () + } +} + +// ----- + +module attributes {llzk.lang} { + poly.template @BoxTemplate { + poly.param @P + struct.def @Box { + function.def @compute() -> !struct.type<@BoxTemplate::@Box<[@P]>> { + %self = struct.new : <@BoxTemplate::@Box<[@P]>> + function.return %self : !struct.type<@BoxTemplate::@Box<[@P]>> + } + function.def @constrain(%self: !struct.type<@BoxTemplate::@Box<[@P]>>) { + function.return + } + } + } + + poly.template @Target { + poly.param @F : !felt.type + function.def @accept(%value: !struct.type<@BoxTemplate::@Box<[@F]>>) { + function.return + } + verif.contract @Base for @Target::@accept ( + %value: !struct.type<@BoxTemplate::@Box<[@F]>> + ) { + %ok = arith.constant true + verif.ensure_compute %ok + } + } + + module @globals { + global.def const @G : !felt.type<"bn128"> = 35 + } + + function.def @global_value_conflict( + %value: !struct.type<@BoxTemplate::@Box<[#felt>]>> + ) { + function.return + } + verif.contract @GlobalValueConflict for @global_value_conflict ( + %value: !struct.type<@BoxTemplate::@Box<[#felt>]>> + ) { + // expected-error@+1 {{'verif.include' op template instantiation value '@globals::@G' for parameter "@F" conflicts with value '#felt> : !felt.type<"bn128">' inferred from contract type signature}} + verif.include @Target::@Base<[@globals::@G]>(%value) : + (!struct.type<@BoxTemplate::@Box<[#felt>]>>) -> () + } +} diff --git a/test/Dialect/Verif/include_with_symbolic_felt_restriction_pass.llzk b/test/Dialect/Verif/include_with_symbolic_felt_restriction_pass.llzk new file mode 100644 index 0000000000..7562a751c6 --- /dev/null +++ b/test/Dialect/Verif/include_with_symbolic_felt_restriction_pass.llzk @@ -0,0 +1,138 @@ +// RUN: llzk-opt -split-input-file %s + +module attributes {llzk.lang} { + poly.template @BoxTemplate { + poly.param @P + struct.def @Box { + function.def @compute() -> !struct.type<@BoxTemplate::@Box<[@P]>> { + %self = struct.new : <@BoxTemplate::@Box<[@P]>> + function.return %self : !struct.type<@BoxTemplate::@Box<[@P]>> + } + function.def @constrain(%self: !struct.type<@BoxTemplate::@Box<[@P]>>) { + function.return + } + } + } + + poly.template @Target { + poly.param @F : !felt.type + function.def @accept(%value: !struct.type<@BoxTemplate::@Box<[@F]>>) { + function.return + } + verif.contract @Base for @Target::@accept ( + %value: !struct.type<@BoxTemplate::@Box<[@F]>> + ) { + %ok = arith.constant true + verif.ensure_compute %ok + } + } + + poly.template @Caller { + poly.param @G : !felt.type<"bn128"> + poly.param @H : !felt.type<"bn128"> + function.def @explicit(%value: !struct.type<@BoxTemplate::@Box<[@G]>>) { + function.return + } + function.def @inferred(%value: !struct.type<@BoxTemplate::@Box<[@G]>>) { + function.return + } + verif.contract @Explicit for @Caller::@explicit ( + %value: !struct.type<@BoxTemplate::@Box<[@G]>> + ) { + verif.include @Target::@Base<[@G]>(%value) : + (!struct.type<@BoxTemplate::@Box<[@G]>>) -> () + } + verif.contract @Inferred for @Caller::@inferred ( + %value: !struct.type<@BoxTemplate::@Box<[@G]>> + ) { + verif.include @Target::@Base(%value) : + (!struct.type<@BoxTemplate::@Box<[@G]>>) -> () + } + function.def @concrete(%value: !struct.type<@BoxTemplate::@Box<[ + #felt> + ]>>) { + function.return + } + verif.contract @Concrete for @Caller::@concrete ( + %value: !struct.type<@BoxTemplate::@Box<[ + #felt> + ]>> + ) { + verif.include @Target::@Base<[@G]>(%value) : + (!struct.type<@BoxTemplate::@Box<[ + #felt> + ]>>) -> () + } + function.def @fieldless_concrete( + %value: !struct.type<@BoxTemplate::@Box<[#felt]>> + ) { + function.return + } + verif.contract @FieldlessConcrete for @Caller::@fieldless_concrete ( + %value: !struct.type<@BoxTemplate::@Box<[#felt]>> + ) { + verif.include @Target::@Base<[@G]>(%value) : + (!struct.type<@BoxTemplate::@Box<[#felt]>>) -> () + } + function.def @concrete_vs_symbol(%value: !struct.type<@BoxTemplate::@Box<[@G]>>) { + function.return + } + verif.contract @ConcreteVsSymbol for @Caller::@concrete_vs_symbol ( + %value: !struct.type<@BoxTemplate::@Box<[@G]>> + ) { + verif.include @Target::@Base<[#felt>]>(%value) : + (!struct.type<@BoxTemplate::@Box<[@G]>>) -> () + } + function.def @symbols_match(%value: !struct.type<@BoxTemplate::@Box<[@H]>>) { + function.return + } + verif.contract @SymbolsMatch for @Caller::@symbols_match ( + %value: !struct.type<@BoxTemplate::@Box<[@H]>> + ) { + verif.include @Target::@Base<[@G]>(%value) : + (!struct.type<@BoxTemplate::@Box<[@H]>>) -> () + } + } + + module @globals { + global.def const @G : !felt.type<"bn128"> = 35 + } + + function.def @qualified(%value: !struct.type<@BoxTemplate::@Box<[@globals::@G]>>) { + function.return + } + verif.contract @Qualified for @qualified ( + %value: !struct.type<@BoxTemplate::@Box<[@globals::@G]>> + ) { + verif.include @Target::@Base<[@globals::@G]>(%value) : + (!struct.type<@BoxTemplate::@Box<[@globals::@G]>>) -> () + } + + function.def @qualified_concrete(%value: !struct.type<@BoxTemplate::@Box<[ + #felt> + ]>>) { + function.return + } + verif.contract @QualifiedConcrete for @qualified_concrete ( + %value: !struct.type<@BoxTemplate::@Box<[ + #felt> + ]>> + ) { + verif.include @Target::@Base<[@globals::@G]>(%value) : + (!struct.type<@BoxTemplate::@Box<[ + #felt> + ]>>) -> () + } + + function.def @qualified_fieldless_concrete( + %value: !struct.type<@BoxTemplate::@Box<[#felt]>> + ) { + function.return + } + verif.contract @QualifiedFieldlessConcrete for @qualified_fieldless_concrete ( + %value: !struct.type<@BoxTemplate::@Box<[#felt]>> + ) { + verif.include @Target::@Base<[@globals::@G]>(%value) : + (!struct.type<@BoxTemplate::@Box<[#felt]>>) -> () + } +} diff --git a/test/Transforms/Flattening/instantiate_expr_fail.llzk b/test/Transforms/Flattening/instantiate_expr_fail.llzk new file mode 100644 index 0000000000..31b69c0409 --- /dev/null +++ b/test/Transforms/Flattening/instantiate_expr_fail.llzk @@ -0,0 +1,59 @@ +// RUN: llzk-opt -split-input-file -llzk-flatten -verify-diagnostics %s 2>&1 | FileCheck --enable-var-scope %s + +module attributes {llzk.lang, llzk.main = !struct.type<@StructExpr::@Value<[5]>>} { + poly.template @StructExpr { + poly.param @N : index + poly.expr @NestedN { + // expected-error@+1 {{'scf.execute_region' op cannot fold concrete template expression}} + %value = scf.execute_region -> index { + %n = poly.read_const @N : index + scf.yield %n : index + } + poly.yield %value : index + } + struct.def @Value { + function.def @compute() -> !struct.type<@StructExpr::@Value<[@N]>> { + %value = poly.read_const @NestedN : index + %self = struct.new : <@StructExpr::@Value<[@N]>> + function.return %self : !struct.type<@StructExpr::@Value<[@N]>> + } + function.def @constrain(%self: !struct.type<@StructExpr::@Value<[@N]>>) { + function.return + } + } + } +} + +// ----- + +module attributes {llzk.lang} { + poly.template @FunctionExpr { + poly.param @N : index + poly.expr @NestedN { + // expected-error@+1 {{'scf.execute_region' op cannot fold concrete template expression}} + %value = scf.execute_region -> index { + %n = poly.read_const @N : index + scf.yield %n : index + } + poly.yield %value : index + } + function.def @value() -> index { + %value = poly.read_const @NestedN : index + function.return %value : index + } + } + + struct.def @Main { + function.def @compute() -> !struct.type<@Main> { + %self = struct.new : <@Main> + %value = function.call @FunctionExpr::@value<[5]>() : () -> index + function.return %self : !struct.type<@Main> + } + function.def @constrain(%self: !struct.type<@Main>) { + function.return + } + } +} + +// Verify the command-level pass failure after the split-local diagnostics. +// CHECK: llzk-flatten failed while instantiating the main struct diff --git a/test/Transforms/Flattening/instantiate_expr_felt_typevar_completion.llzk b/test/Transforms/Flattening/instantiate_expr_felt_typevar_completion.llzk new file mode 100644 index 0000000000..ef9d74eb02 --- /dev/null +++ b/test/Transforms/Flattening/instantiate_expr_felt_typevar_completion.llzk @@ -0,0 +1,147 @@ +// RUN: llzk-opt -split-input-file -llzk-flatten %s | FileCheck %s + +// A fieldless FeltConstAttr is materialized to the converted fielded type during direct +// specialization of a free function. +module attributes {llzk.lang} { + poly.template @FeltDirect { + poly.param @Num + poly.param @Ty : !poly.tvar<@Ty> + poly.expr @Value { + %num = poly.read_const @Num : !poly.tvar<@Ty> + %value = poly.unifiable_cast %num : (!poly.tvar<@Ty>) -> !felt.type<"bn128"> + poly.yield %value : !felt.type<"bn128"> + } + function.def @value() -> !felt.type<"bn128"> { + %value = poly.read_const @Value : !felt.type<"bn128"> + function.return %value : !felt.type<"bn128"> + } + } + + function.def @main() -> !felt.type<"bn128"> { + %value = function.call @FeltDirect::@value<[#felt, !felt.type<"bn128">]>() : () -> !felt.type<"bn128"> + function.return %value : !felt.type<"bn128"> + } +} + +// CHECK-LABEL: module attributes {llzk.lang} { +// CHECK-NEXT: function.def @"FeltDirect_f<35>_f_value"() -> !felt.type<"bn128"> { +// CHECK-NEXT: %[[VAL_0:[0-9a-zA-Z_\.]+]] = felt.const 35 : <"bn128"> +// CHECK-NEXT: function.return %[[VAL_0]] : !felt.type<"bn128"> +// CHECK-NEXT: } +// CHECK-NEXT: function.def @main() -> !felt.type<"bn128"> { +// CHECK-NEXT: %[[VAL_1:[0-9a-zA-Z_\.]+]] = function.call @"FeltDirect_f<35>_f_value"() : () -> !felt.type<"bn128"> +// CHECK-NEXT: function.return %[[VAL_1]] : !felt.type<"bn128"> +// CHECK-NEXT: } +// CHECK-NEXT: } + +// ----- + +// The same fielded result survives a partial free-function specialization and is materialized +// when the remaining fielded felt parameter becomes concrete. +module attributes {llzk.lang} { + poly.template @FeltPartial { + poly.param @Num + poly.param @Ty : !poly.tvar<@Ty> + poly.param @M : !felt.type<"bn128"> + poly.expr @Sum { + %num = poly.read_const @Num : !poly.tvar<@Ty> + %num_felt = poly.unifiable_cast %num : (!poly.tvar<@Ty>) -> !felt.type<"bn128"> + %m = poly.read_const @M : !felt.type<"bn128"> + %sum = felt.add %num_felt, %m : !felt.type<"bn128">, !felt.type<"bn128"> + poly.yield %sum : !felt.type<"bn128"> + } + function.def @value() -> !felt.type<"bn128"> { + %sum = poly.read_const @Sum : !felt.type<"bn128"> + function.return %sum : !felt.type<"bn128"> + } + } + + poly.template @Bridge { + poly.param @M : !felt.type<"bn128"> + function.def @value() -> !felt.type<"bn128"> { + %sum = function.call @FeltPartial::@value<[#felt, !felt.type<"bn128">, @M]>() : () -> !felt.type<"bn128"> + function.return %sum : !felt.type<"bn128"> + } + } + + function.def @main() -> !felt.type<"bn128"> { + %value = function.call @Bridge::@value<[#felt]>() : () -> !felt.type<"bn128"> + function.return %value : !felt.type<"bn128"> + } +} + +// CHECK-LABEL: module attributes {llzk.lang} { +// CHECK-NEXT: function.def @"FeltPartial_f<35>_f_f<7:5:bn128>_value"() -> !felt.type<"bn128"> { +// CHECK-NEXT: %[[VAL_0:[0-9a-zA-Z_\.]+]] = felt.const 42 : <"bn128"> +// CHECK-NEXT: function.return %[[VAL_0]] : !felt.type<"bn128"> +// CHECK-NEXT: } +// CHECK-NEXT: function.def @"Bridge_f<7:5:bn128>_value"() -> !felt.type<"bn128"> { +// CHECK-NEXT: %[[VAL_1:[0-9a-zA-Z_\.]+]] = function.call @"FeltPartial_f<35>_f_f<7:5:bn128>_value"() : () -> !felt.type<"bn128"> +// CHECK-NEXT: function.return %[[VAL_1]] : !felt.type<"bn128"> +// CHECK-NEXT: } +// CHECK-NEXT: function.def @main() -> !felt.type<"bn128"> { +// CHECK-NEXT: %[[VAL_2:[0-9a-zA-Z_\.]+]] = function.call @"Bridge_f<7:5:bn128>_value"() : () -> !felt.type<"bn128"> +// CHECK-NEXT: function.return %[[VAL_2]] : !felt.type<"bn128"> +// CHECK-NEXT: } +// CHECK-NEXT: } + +// ----- + +// Struct instantiation uses the same converted fielded constant in its generated compute body. +module attributes {llzk.lang, llzk.main = !struct.type<@Main>} { + poly.template @FeltStruct { + poly.param @Num + poly.param @Ty : !poly.tvar<@Ty> + poly.expr @ValueExpr { + %num = poly.read_const @Num : !poly.tvar<@Ty> + %value = poly.unifiable_cast %num : (!poly.tvar<@Ty>) -> !felt.type<"bn128"> + poly.yield %value : !felt.type<"bn128"> + } + struct.def @Value { + struct.member @result : !felt.type<"bn128"> + function.def @compute() -> !struct.type<@FeltStruct::@Value<[@Num, @Ty]>> { + %value = poly.read_const @ValueExpr : !felt.type<"bn128"> + %self = struct.new : !struct.type<@FeltStruct::@Value<[@Num, @Ty]>> + struct.writem %self[@result] = %value : !struct.type<@FeltStruct::@Value<[@Num, @Ty]>>, !felt.type<"bn128"> + function.return %self : !struct.type<@FeltStruct::@Value<[@Num, @Ty]>> + } + function.def @constrain(%self: !struct.type<@FeltStruct::@Value<[@Num, @Ty]>>) { + function.return + } + } + } + + struct.def @Main { + struct.member @value : !struct.type<@FeltStruct::@Value<[#felt, !felt.type<"bn128">]>> + function.def @compute() -> !struct.type<@Main> { + %self = struct.new : !struct.type<@Main> + function.return %self : !struct.type<@Main> + } + function.def @constrain(%self: !struct.type<@Main>) { function.return } + } +} + +// CHECK-LABEL: module attributes {llzk.lang, llzk.main = !struct.type<@Main>} { +// CHECK-NEXT: struct.def @"FeltStruct_f<35>_f_Value" { +// CHECK-NEXT: struct.member @result : !felt.type<"bn128"> +// CHECK-NEXT: function.def @compute() -> !struct.type<@"FeltStruct_f<35>_f_Value"> attributes {function.allow_witness} { +// CHECK-NEXT: %[[VAL_0:[0-9a-zA-Z_\.]+]] = felt.const 35 : <"bn128"> +// CHECK-NEXT: %[[VAL_1:[0-9a-zA-Z_\.]+]] = struct.new : <@"FeltStruct_f<35>_f_Value"> +// CHECK-NEXT: struct.writem %[[VAL_1]][@result] = %[[VAL_0]] : <@"FeltStruct_f<35>_f_Value">, !felt.type<"bn128"> +// CHECK-NEXT: function.return %[[VAL_1]] : !struct.type<@"FeltStruct_f<35>_f_Value"> +// CHECK-NEXT: } +// CHECK-NEXT: function.def @constrain(%[[VAL_2:[0-9a-zA-Z_\.]+]]: !struct.type<@"FeltStruct_f<35>_f_Value">) attributes {function.allow_constraint} { +// CHECK-NEXT: function.return +// CHECK-NEXT: } +// CHECK-NEXT: } +// CHECK-NEXT: struct.def @Main { +// CHECK-NEXT: struct.member @value : !struct.type<@"FeltStruct_f<35>_f_Value"> +// CHECK-NEXT: function.def @compute() -> !struct.type<@Main> attributes {function.allow_witness} { +// CHECK-NEXT: %[[VAL_3:[0-9a-zA-Z_\.]+]] = struct.new : <@Main> +// CHECK-NEXT: function.return %[[VAL_3]] : !struct.type<@Main> +// CHECK-NEXT: } +// CHECK-NEXT: function.def @constrain(%[[VAL_4:[0-9a-zA-Z_\.]+]]: !struct.type<@Main>) attributes {function.allow_constraint} { +// CHECK-NEXT: function.return +// CHECK-NEXT: } +// CHECK-NEXT: } +// CHECK-NEXT: } diff --git a/test/Transforms/Flattening/instantiate_expr_partial.llzk b/test/Transforms/Flattening/instantiate_expr_partial.llzk new file mode 100644 index 0000000000..d210c29205 --- /dev/null +++ b/test/Transforms/Flattening/instantiate_expr_partial.llzk @@ -0,0 +1,396 @@ +// RUN: llzk-opt -split-input-file -llzk-flatten %s | FileCheck %s + +#id = affine_map<(i)->(i)> +module attributes {llzk.lang, llzk.main = !struct.type<@Outer::@Wrapper<[7]>>} { + poly.template @Inner { + poly.param @N : index + poly.param @M : index + poly.expr @TwiceM { + %m = poly.read_const @M : index + %two = arith.constant 2 : index + %result = arith.muli %m, %two : index + poly.yield %result : index + } + poly.expr @NPlusM { + %n = poly.read_const @N : index + %m = poly.read_const @M : index + %result = arith.addi %n, %m : index + poly.yield %result : index + } + struct.def @Value { + struct.member @values : !array.type<#id x !felt.type> + function.def @compute() -> !struct.type<@Inner::@Value<[@N, @M]>> { + %sum = poly.read_const @NPlusM : index + %self = struct.new : <@Inner::@Value<[@N, @M]>> + %values = array.new{(%sum)[]} : !array.type<#id x !felt.type> + struct.writem %self[@values] = %values : <@Inner::@Value<[@N, @M]>>, !array.type<#id x !felt.type> + function.return %self : !struct.type<@Inner::@Value<[@N, @M]>> + } + function.def @constrain(%self: !struct.type<@Inner::@Value<[@N, @M]>>) { + function.return + } + } + } + + poly.template @Outer { + poly.param @M : index + struct.def @Wrapper { + struct.member @value : !struct.type<@Inner::@Value<[5, @M]>> + function.def @compute() -> !struct.type<@Outer::@Wrapper<[@M]>> { + %self = struct.new : <@Outer::@Wrapper<[@M]>> + function.return %self : !struct.type<@Outer::@Wrapper<[@M]>> + } + function.def @constrain(%self: !struct.type<@Outer::@Wrapper<[@M]>>) { + function.return + } + } + } +} + +// CHECK: #[[$ATTR_0:[0-9a-zA-Z_\.]+]] = affine_map<(d0) -> (d0)> +// CHECK-LABEL: module attributes {llzk.lang, llzk.main = !struct.type<@Outer_7_Wrapper>} { +// CHECK-NEXT: struct.def @Inner_5_7_Value { +// CHECK-NEXT: struct.member @values : !array.type<12 x !felt.type> +// CHECK-NEXT: function.def @compute() -> !struct.type<@Inner_5_7_Value> attributes {function.allow_witness} { +// CHECK-NEXT: %[[VAL_0:[0-9a-zA-Z_\.]+]] = struct.new : <@Inner_5_7_Value> +// CHECK-NEXT: %[[VAL_1:[0-9a-zA-Z_\.]+]] = array.new : <12 x !felt.type> +// CHECK-NEXT: struct.writem %[[VAL_0]][@values] = %[[VAL_1]] : <@Inner_5_7_Value>, !array.type<12 x !felt.type> +// CHECK-NEXT: function.return %[[VAL_0]] : !struct.type<@Inner_5_7_Value> +// CHECK-NEXT: } +// CHECK-NEXT: function.def @constrain(%[[VAL_2:[0-9a-zA-Z_\.]+]]: !struct.type<@Inner_5_7_Value>) attributes {function.allow_constraint} { +// CHECK-NEXT: function.return +// CHECK-NEXT: } +// CHECK-NEXT: } +// CHECK-NEXT: poly.template @"Inner_5_\1A" { +// CHECK-NEXT: poly.param @M : index +// CHECK-NEXT: poly.expr @NPlusM { +// CHECK-NEXT: %[[VAL_3:[0-9a-zA-Z_\.]+]] = arith.constant 5 : index +// CHECK-NEXT: %[[VAL_4:[0-9a-zA-Z_\.]+]] = poly.read_const @M : index +// CHECK-NEXT: %[[VAL_5:[0-9a-zA-Z_\.]+]] = arith.addi %[[VAL_4]], %[[VAL_3]] : index +// CHECK-NEXT: poly.yield %[[VAL_5]] : index +// CHECK-NEXT: } +// CHECK-NEXT: struct.def @Value { +// CHECK-NEXT: struct.member @values : !array.type<#[[$ATTR_0]] x !felt.type> +// CHECK-NEXT: function.def @compute() -> !struct.type<@"Inner_5_\1A"::@Value<[@M]>> attributes {function.allow_witness} { +// CHECK-NEXT: %[[VAL_6:[0-9a-zA-Z_\.]+]] = poly.read_const @NPlusM : index +// CHECK-NEXT: %[[VAL_7:[0-9a-zA-Z_\.]+]] = struct.new : <@"Inner_5_\1A"::@Value<[@M]>> +// CHECK-NEXT: %[[VAL_8:[0-9a-zA-Z_\.]+]] = array.new{(%[[VAL_6]])} : <#[[$ATTR_0]] x !felt.type> +// CHECK-NEXT: struct.writem %[[VAL_7]][@values] = %[[VAL_8]] : <@"Inner_5_\1A"::@Value<[@M]>>, !array.type<#[[$ATTR_0]] x !felt.type> +// CHECK-NEXT: function.return %[[VAL_7]] : !struct.type<@"Inner_5_\1A"::@Value<[@M]>> +// CHECK-NEXT: } +// CHECK-NEXT: function.def @constrain(%[[VAL_9:[0-9a-zA-Z_\.]+]]: !struct.type<@"Inner_5_\1A"::@Value<[@M]>>) attributes {function.allow_constraint} { +// CHECK-NEXT: function.return +// CHECK-NEXT: } +// CHECK-NEXT: } +// CHECK-NEXT: } {poly.name_pattern = ["Inner_5_", ""]} +// CHECK-NEXT: struct.def @Outer_7_Wrapper { +// CHECK-NEXT: struct.member @value : !struct.type<@Inner_5_7_Value> +// CHECK-NEXT: function.def @compute() -> !struct.type<@Outer_7_Wrapper> attributes {function.allow_witness} { +// CHECK-NEXT: %[[VAL_10:[0-9a-zA-Z_\.]+]] = struct.new : <@Outer_7_Wrapper> +// CHECK-NEXT: function.return %[[VAL_10]] : !struct.type<@Outer_7_Wrapper> +// CHECK-NEXT: } +// CHECK-NEXT: function.def @constrain(%[[VAL_11:[0-9a-zA-Z_\.]+]]: !struct.type<@Outer_7_Wrapper>) attributes {function.allow_constraint} { +// CHECK-NEXT: function.return +// CHECK-NEXT: } +// CHECK-NEXT: } +// CHECK-NEXT: } + +// ----- + +module attributes {llzk.lang} { + poly.template @InnerFunc { + poly.param @N : index + poly.param @M : index + poly.expr @TwiceM { + %m = poly.read_const @M : index + %two = arith.constant 2 : index + %result = arith.muli %m, %two : index + poly.yield %result : index + } + poly.expr @NPlusM { + %n = poly.read_const @N : index + %m = poly.read_const @M : index + %result = arith.addi %n, %m : index + poly.yield %result : index + } + function.def @value(%input: !array.type<@N,@M x !felt.type>) -> index { + %sum = poly.read_const @NPlusM : index + function.return %sum : index + } + } + + poly.template @OuterFunc { + poly.param @M : index + function.def @value(%input: !array.type<5,@M x !felt.type>) -> index { + %result = function.call @InnerFunc::@value(%input) : (!array.type<5,@M x !felt.type>) -> index + function.return %result : index + } + } + + struct.def @Main { + function.def @compute(%input: !array.type<5,7 x !felt.type>) -> !struct.type<@Main> { + %self = struct.new : <@Main> + %result = function.call @OuterFunc::@value(%input) : (!array.type<5,7 x !felt.type>) -> index + function.return %self : !struct.type<@Main> + } + function.def @constrain(%self: !struct.type<@Main>, %input: !array.type<5,7 x !felt.type>) { + function.return + } + } +} + +// CHECK-LABEL: module attributes {llzk.lang} { +// CHECK-NEXT: function.def @InnerFunc_5_7_value(%[[VAL_0:[0-9a-zA-Z_\.]+]]: !array.type<5,7 x !felt.type>) -> index { +// CHECK-NEXT: %[[VAL_1:[0-9a-zA-Z_\.]+]] = arith.constant 12 : index +// CHECK-NEXT: function.return %[[VAL_1]] : index +// CHECK-NEXT: } +// CHECK-NEXT: function.def @OuterFunc_7_value(%[[VAL_2:[0-9a-zA-Z_\.]+]]: !array.type<5,7 x !felt.type>) -> index { +// CHECK-NEXT: %[[VAL_3:[0-9a-zA-Z_\.]+]] = function.call @InnerFunc_5_7_value(%[[VAL_2]]) : (!array.type<5,7 x !felt.type>) -> index +// CHECK-NEXT: function.return %[[VAL_3]] : index +// CHECK-NEXT: } +// CHECK-NEXT: struct.def @Main { +// CHECK-NEXT: function.def @compute(%[[VAL_4:[0-9a-zA-Z_\.]+]]: !array.type<5,7 x !felt.type>) -> !struct.type<@Main> attributes {function.allow_witness} { +// CHECK-NEXT: %[[VAL_5:[0-9a-zA-Z_\.]+]] = struct.new : <@Main> +// CHECK-NEXT: %[[VAL_6:[0-9a-zA-Z_\.]+]] = function.call @OuterFunc_7_value(%[[VAL_4]]) : (!array.type<5,7 x !felt.type>) -> index +// CHECK-NEXT: function.return %[[VAL_5]] : !struct.type<@Main> +// CHECK-NEXT: } +// CHECK-NEXT: function.def @constrain(%[[VAL_7:[0-9a-zA-Z_\.]+]]: !struct.type<@Main>, %[[VAL_8:[0-9a-zA-Z_\.]+]]: !array.type<5,7 x !felt.type>) attributes {function.allow_constraint} { +// CHECK-NEXT: function.return +// CHECK-NEXT: } +// CHECK-NEXT: } +// CHECK-NEXT: } + +// ----- + +module attributes {llzk.lang} { + poly.template @Inner { + poly.param @N : index + poly.param @M : index + poly.expr @NestedM { + %value = scf.execute_region -> index { + %m = poly.read_const @M : index + scf.yield %m : index + } + poly.yield %value : index + } + function.def @value() -> index { + %value = poly.read_const @NestedM : index + function.return %value : index + } + } + + poly.template @Outer { + poly.param @M : index + function.def @value() -> index { + %value = function.call @Inner::@value<[5, @M]>() : () -> index + function.return %value : index + } + } +} + +// CHECK-LABEL: module attributes {llzk.lang} { +// CHECK-NEXT: poly.template @"Inner_5_\1A" { +// CHECK-NEXT: poly.param @M : index +// CHECK-NEXT: poly.expr @NestedM { +// CHECK-NEXT: %[[VAL_0:[0-9a-zA-Z_\.]+]] = scf.execute_region -> index { +// CHECK-NEXT: %[[VAL_1:[0-9a-zA-Z_\.]+]] = poly.read_const @M : index +// CHECK-NEXT: scf.yield %[[VAL_1]] : index +// CHECK-NEXT: } +// CHECK-NEXT: poly.yield %[[VAL_0]] : index +// CHECK-NEXT: } +// CHECK-NEXT: function.def @value() -> index { +// CHECK-NEXT: %[[VAL_2:[0-9a-zA-Z_\.]+]] = poly.read_const @NestedM : index +// CHECK-NEXT: function.return %[[VAL_2]] : index +// CHECK-NEXT: } +// CHECK-NEXT: } {poly.name_pattern = ["Inner_5_", ""]} +// CHECK-NEXT: poly.template @Outer { +// CHECK-NEXT: poly.param @M : index +// CHECK-NEXT: function.def @value() -> index { +// CHECK-NEXT: %[[VAL_3:[0-9a-zA-Z_\.]+]] = function.call @"Inner_5_\1A"::@value<[@M]>() : () -> index +// CHECK-NEXT: function.return %[[VAL_3]] : index +// CHECK-NEXT: } +// CHECK-NEXT: } +// CHECK-NEXT: } + +// ----- + +module attributes {llzk.lang} { + poly.template @InnerFunc { + poly.param @N : index + poly.param @M : index + poly.expr @TwiceM { + %m = poly.read_const @M : index + %two = arith.constant 2 : index + %result = arith.muli %m, %two : index + poly.yield %result : index + } + poly.expr @NPlusM { + %n = poly.read_const @N : index + %m = poly.read_const @M : index + %result = arith.addi %n, %m : index + poly.yield %result : index + } + function.def @value(%input: !array.type<@N,@M x !felt.type>) -> index { + %sum = poly.read_const @NPlusM : index + function.return %sum : index + } + } + + poly.template @OuterFunc { + poly.param @M : index + function.def @value(%input: !array.type<5,@M x !felt.type>) -> index { + %result = function.call @InnerFunc::@value(%input) : (!array.type<5,@M x !felt.type>) -> index + function.return %result : index + } + } +} + +// CHECK-LABEL: module attributes {llzk.lang} { +// CHECK-NEXT: poly.template @"InnerFunc_5_\1A" { +// CHECK-NEXT: poly.param @M : index +// CHECK-NEXT: poly.expr @NPlusM { +// CHECK-NEXT: %[[VAL_0:[0-9a-zA-Z_\.]+]] = arith.constant 5 : index +// CHECK-NEXT: %[[VAL_1:[0-9a-zA-Z_\.]+]] = poly.read_const @M : index +// CHECK-NEXT: %[[VAL_2:[0-9a-zA-Z_\.]+]] = arith.addi %[[VAL_1]], %[[VAL_0]] : index +// CHECK-NEXT: poly.yield %[[VAL_2]] : index +// CHECK-NEXT: } +// CHECK-NEXT: function.def @value(%[[VAL_3:[0-9a-zA-Z_\.]+]]: !array.type<5,@M x !felt.type>) -> index { +// CHECK-NEXT: %[[VAL_4:[0-9a-zA-Z_\.]+]] = poly.read_const @NPlusM : index +// CHECK-NEXT: function.return %[[VAL_4]] : index +// CHECK-NEXT: } +// CHECK-NEXT: } {poly.name_pattern = ["InnerFunc_5_", ""]} +// CHECK-NEXT: poly.template @OuterFunc { +// CHECK-NEXT: poly.param @M : index +// CHECK-NEXT: function.def @value(%[[VAL_5:[0-9a-zA-Z_\.]+]]: !array.type<5,@M x !felt.type>) -> index { +// CHECK-NEXT: %[[VAL_6:[0-9a-zA-Z_\.]+]] = function.call @"InnerFunc_5_\1A"::@value(%[[VAL_5]]) : (!array.type<5,@M x !felt.type>) -> index +// CHECK-NEXT: function.return %[[VAL_6]] : index +// CHECK-NEXT: } +// CHECK-NEXT: } +// CHECK-NEXT: } + +// ----- + +module attributes {llzk.lang} { + poly.template @Inner { + poly.param @Num + poly.param @Ty : !poly.tvar<@Ty> + poly.param @M : index + poly.expr @Sum { + %num = poly.read_const @Num : !poly.tvar<@Ty> + %cast = poly.unifiable_cast %num : (!poly.tvar<@Ty>) -> index + %m = poly.read_const @M : index + %sum = arith.addi %m, %cast : index + poly.yield %sum : index + } + struct.def @Value { + function.def @compute() -> !struct.type<@Inner::@Value<[@Num, @Ty, @M]>> { + %sum = poly.read_const @Sum : index + %self = struct.new : <@Inner::@Value<[@Num, @Ty, @M]>> + function.return %self : !struct.type<@Inner::@Value<[@Num, @Ty, @M]>> + } + function.def @constrain(%self: !struct.type<@Inner::@Value<[@Num, @Ty, @M]>>) { + function.return + } + } + } + + poly.template @Outer { + poly.param @M : index + struct.def @Wrapper { + struct.member @value : !struct.type<@Inner::@Value<[35, index, @M]>> + function.def @compute() -> !struct.type<@Outer::@Wrapper<[@M]>> { + %self = struct.new : <@Outer::@Wrapper<[@M]>> + function.return %self : !struct.type<@Outer::@Wrapper<[@M]>> + } + function.def @constrain(%self: !struct.type<@Outer::@Wrapper<[@M]>>) { + function.return + } + } + } +} + +// CHECK-LABEL: module attributes {llzk.lang} { +// CHECK-NEXT: poly.template @"Inner_35_i_\1A" { +// CHECK-NEXT: poly.param @M : index +// CHECK-NEXT: poly.expr @Sum { +// CHECK-NEXT: %[[VAL_0:[0-9a-zA-Z_\.]+]] = arith.constant 35 : index +// CHECK-NEXT: %[[VAL_1:[0-9a-zA-Z_\.]+]] = poly.read_const @M : index +// CHECK-NEXT: %[[VAL_2:[0-9a-zA-Z_\.]+]] = arith.addi %[[VAL_1]], %[[VAL_0]] : index +// CHECK-NEXT: poly.yield %[[VAL_2]] : index +// CHECK-NEXT: } +// CHECK-NEXT: struct.def @Value { +// CHECK-NEXT: function.def @compute() -> !struct.type<@"Inner_35_i_\1A"::@Value<[@M]>> attributes {function.allow_witness} { +// CHECK-NEXT: %[[VAL_3:[0-9a-zA-Z_\.]+]] = struct.new : <@"Inner_35_i_\1A"::@Value<[@M]>> +// CHECK-NEXT: function.return %[[VAL_3]] : !struct.type<@"Inner_35_i_\1A"::@Value<[@M]>> +// CHECK-NEXT: } +// CHECK-NEXT: function.def @constrain(%[[VAL_4:[0-9a-zA-Z_\.]+]]: !struct.type<@"Inner_35_i_\1A"::@Value<[@M]>>) attributes {function.allow_constraint} { +// CHECK-NEXT: function.return +// CHECK-NEXT: } +// CHECK-NEXT: } +// CHECK-NEXT: } {poly.name_pattern = ["Inner_35_i_", ""]} +// CHECK-NEXT: poly.template @Outer { +// CHECK-NEXT: poly.param @M : index +// CHECK-NEXT: struct.def @Wrapper { +// CHECK-NEXT: struct.member @value : !struct.type<@"Inner_35_i_\1A"::@Value<[@M]>> +// CHECK-NEXT: function.def @compute() -> !struct.type<@Outer::@Wrapper<[@M]>> attributes {function.allow_witness} { +// CHECK-NEXT: %[[VAL_5:[0-9a-zA-Z_\.]+]] = struct.new : <@Outer::@Wrapper<[@M]>> +// CHECK-NEXT: function.return %[[VAL_5]] : !struct.type<@Outer::@Wrapper<[@M]>> +// CHECK-NEXT: } +// CHECK-NEXT: function.def @constrain(%[[VAL_6:[0-9a-zA-Z_\.]+]]: !struct.type<@Outer::@Wrapper<[@M]>>) attributes {function.allow_constraint} { +// CHECK-NEXT: function.return +// CHECK-NEXT: } +// CHECK-NEXT: } +// CHECK-NEXT: } +// CHECK-NEXT: } + +// ----- + +module attributes {llzk.lang} { + poly.template @Inner { + poly.param @Num + poly.param @Ty : !poly.tvar<@Ty> + poly.param @M : index + poly.expr @Sum { + %num = poly.read_const @Num : !poly.tvar<@Ty> + %cast = poly.unifiable_cast %num : (!poly.tvar<@Ty>) -> index + %m = poly.read_const @M : index + %sum = arith.addi %m, %cast : index + poly.yield %sum : index + } + function.def @value() -> index { + %sum = poly.read_const @Sum : index + function.return %sum : index + } + } + + poly.template @Outer { + poly.param @M : index + function.def @value() -> index { + %sum = function.call @Inner::@value<[35, index, @M]>() : () -> index + function.return %sum : index + } + } +} + +// CHECK-LABEL: module attributes {llzk.lang} { +// CHECK-NEXT: poly.template @"Inner_35_i_\1A" { +// CHECK-NEXT: poly.param @M : index +// CHECK-NEXT: poly.expr @Sum { +// CHECK-NEXT: %[[VAL_0:[0-9a-zA-Z_\.]+]] = arith.constant 35 : index +// CHECK-NEXT: %[[VAL_1:[0-9a-zA-Z_\.]+]] = poly.read_const @M : index +// CHECK-NEXT: %[[VAL_2:[0-9a-zA-Z_\.]+]] = arith.addi %[[VAL_1]], %[[VAL_0]] : index +// CHECK-NEXT: poly.yield %[[VAL_2]] : index +// CHECK-NEXT: } +// CHECK-NEXT: function.def @value() -> index { +// CHECK-NEXT: %[[VAL_3:[0-9a-zA-Z_\.]+]] = poly.read_const @Sum : index +// CHECK-NEXT: function.return %[[VAL_3]] : index +// CHECK-NEXT: } +// CHECK-NEXT: } {poly.name_pattern = ["Inner_35_i_", ""]} +// CHECK-NEXT: poly.template @Outer { +// CHECK-NEXT: poly.param @M : index +// CHECK-NEXT: function.def @value() -> index { +// CHECK-NEXT: %[[VAL_4:[0-9a-zA-Z_\.]+]] = function.call @"Inner_35_i_\1A"::@value<[@M]>() : () -> index +// CHECK-NEXT: function.return %[[VAL_4]] : index +// CHECK-NEXT: } +// CHECK-NEXT: } +// CHECK-NEXT: } diff --git a/test/Transforms/Flattening/instantiate_expr_target_local.llzk b/test/Transforms/Flattening/instantiate_expr_target_local.llzk new file mode 100644 index 0000000000..b4c8479bfc --- /dev/null +++ b/test/Transforms/Flattening/instantiate_expr_target_local.llzk @@ -0,0 +1,131 @@ +// RUN: llzk-opt -split-input-file --pass-pipeline='builtin.module(llzk-flatten{cleanup=disabled})' %s | FileCheck %s + +module attributes {llzk.lang, llzk.main = !struct.type<@UnusedStructExpr::@Value<[5]>>} { + poly.template @UnusedStructExpr { + poly.param @N : index + poly.expr @Unused { + %value = scf.execute_region -> index { + %n = poly.read_const @N : index + scf.yield %n : index + } + poly.yield %value : index + } + function.def @other() -> index { + %value = poly.read_const @Unused : index + function.return %value : index + } + struct.def @Value { + function.def @compute() -> !struct.type<@UnusedStructExpr::@Value<[@N]>> { + %self = struct.new : <@UnusedStructExpr::@Value<[@N]>> + function.return %self : !struct.type<@UnusedStructExpr::@Value<[@N]>> + } + function.def @constrain(%self: !struct.type<@UnusedStructExpr::@Value<[@N]>>) { + function.return + } + } + } +} + +// CHECK-LABEL: module attributes {llzk.lang, llzk.main = !struct.type<@UnusedStructExpr_5_Value>} { +// CHECK-NEXT: struct.def @UnusedStructExpr_5_Value { +// CHECK-NEXT: function.def @compute() -> !struct.type<@UnusedStructExpr_5_Value> attributes {function.allow_witness} { +// CHECK-NEXT: %[[VAL_0:[0-9a-zA-Z_\.]+]] = struct.new : <@UnusedStructExpr_5_Value> +// CHECK-NEXT: function.return %[[VAL_0]] : !struct.type<@UnusedStructExpr_5_Value> +// CHECK-NEXT: } +// CHECK-NEXT: function.def @constrain(%[[VAL_1:[0-9a-zA-Z_\.]+]]: !struct.type<@UnusedStructExpr_5_Value>) attributes {function.allow_constraint} { +// CHECK-NEXT: function.return +// CHECK-NEXT: } +// CHECK-NEXT: } +// CHECK-NEXT: poly.template @UnusedStructExpr { +// CHECK-NEXT: poly.param @N : index +// CHECK-NEXT: poly.expr @Unused { +// CHECK-NEXT: %[[VAL_2:[0-9a-zA-Z_\.]+]] = scf.execute_region -> index { +// CHECK-NEXT: %[[VAL_3:[0-9a-zA-Z_\.]+]] = poly.read_const @N : index +// CHECK-NEXT: scf.yield %[[VAL_3]] : index +// CHECK-NEXT: } +// CHECK-NEXT: poly.yield %[[VAL_2]] : index +// CHECK-NEXT: } +// CHECK-NEXT: function.def @other() -> index { +// CHECK-NEXT: %[[VAL_4:[0-9a-zA-Z_\.]+]] = poly.read_const @Unused : index +// CHECK-NEXT: function.return %[[VAL_4]] : index +// CHECK-NEXT: } +// CHECK-NEXT: struct.def @Value { +// CHECK-NEXT: function.def @compute() -> !struct.type<@UnusedStructExpr::@Value<[@N]>> attributes {function.allow_witness} { +// CHECK-NEXT: %[[VAL_5:[0-9a-zA-Z_\.]+]] = struct.new : <@UnusedStructExpr::@Value<[@N]>> +// CHECK-NEXT: function.return %[[VAL_5]] : !struct.type<@UnusedStructExpr::@Value<[@N]>> +// CHECK-NEXT: } +// CHECK-NEXT: function.def @constrain(%[[VAL_6:[0-9a-zA-Z_\.]+]]: !struct.type<@UnusedStructExpr::@Value<[@N]>>) attributes {function.allow_constraint} { +// CHECK-NEXT: function.return +// CHECK-NEXT: } +// CHECK-NEXT: } +// CHECK-NEXT: } +// CHECK-NEXT: } + +// ----- + +module attributes {llzk.lang} { + poly.template @UnusedFunctionExpr { + poly.param @N : index + poly.expr @Unused { + %value = scf.execute_region -> index { + %n = poly.read_const @N : index + scf.yield %n : index + } + poly.yield %value : index + } + function.def @other() -> index { + %value = poly.read_const @Unused : index + function.return %value : index + } + function.def @value() -> index { + %result = arith.constant 1 : index + function.return %result : index + } + } + + struct.def @Main { + function.def @compute() -> !struct.type<@Main> { + %self = struct.new : <@Main> + %result = function.call @UnusedFunctionExpr::@value<[5]>() : () -> index + function.return %self : !struct.type<@Main> + } + function.def @constrain(%self: !struct.type<@Main>) { + function.return + } + } +} + +// CHECK-LABEL: module attributes {llzk.lang} { +// CHECK-NEXT: function.def @UnusedFunctionExpr_5_value() -> index { +// CHECK-NEXT: %[[VAL_0:[0-9a-zA-Z_\.]+]] = arith.constant 1 : index +// CHECK-NEXT: function.return %[[VAL_0]] : index +// CHECK-NEXT: } +// CHECK-NEXT: poly.template @UnusedFunctionExpr { +// CHECK-NEXT: poly.param @N : index +// CHECK-NEXT: poly.expr @Unused { +// CHECK-NEXT: %[[VAL_1:[0-9a-zA-Z_\.]+]] = scf.execute_region -> index { +// CHECK-NEXT: %[[VAL_2:[0-9a-zA-Z_\.]+]] = poly.read_const @N : index +// CHECK-NEXT: scf.yield %[[VAL_2]] : index +// CHECK-NEXT: } +// CHECK-NEXT: poly.yield %[[VAL_1]] : index +// CHECK-NEXT: } +// CHECK-NEXT: function.def @other() -> index { +// CHECK-NEXT: %[[VAL_3:[0-9a-zA-Z_\.]+]] = poly.read_const @Unused : index +// CHECK-NEXT: function.return %[[VAL_3]] : index +// CHECK-NEXT: } +// CHECK-NEXT: function.def @value() -> index { +// CHECK-NEXT: %[[VAL_4:[0-9a-zA-Z_\.]+]] = arith.constant 1 : index +// CHECK-NEXT: function.return %[[VAL_4]] : index +// CHECK-NEXT: } +// CHECK-NEXT: } +// CHECK-NEXT: struct.def @Main { +// CHECK-NEXT: function.def @compute() -> !struct.type<@Main> attributes {function.allow_witness} { +// CHECK-NEXT: %[[VAL_5:[0-9a-zA-Z_\.]+]] = struct.new : <@Main> +// CHECK-NEXT: %[[VAL_6:[0-9a-zA-Z_\.]+]] = function.call @UnusedFunctionExpr_5_value() : () -> index +// CHECK-NEXT: function.return %[[VAL_5]] : !struct.type<@Main> +// CHECK-NEXT: } +// CHECK-NEXT: function.def @constrain(%[[VAL_7:[0-9a-zA-Z_\.]+]]: !struct.type<@Main>) attributes {function.allow_constraint} { +// CHECK-NEXT: function.return +// CHECK-NEXT: } +// CHECK-NEXT: } +// CHECK-NEXT: } diff --git a/test/Transforms/Flattening/instantiate_expr_typevar_completion.llzk b/test/Transforms/Flattening/instantiate_expr_typevar_completion.llzk new file mode 100644 index 0000000000..1e60604bf7 --- /dev/null +++ b/test/Transforms/Flattening/instantiate_expr_typevar_completion.llzk @@ -0,0 +1,264 @@ +// RUN: llzk-opt -split-input-file -llzk-flatten %s | FileCheck %s + +// A fully concrete type-variable expression is normalized before direct evaluation. +module attributes {llzk.lang} { + poly.template @InnerDirect { + poly.param @Num + poly.param @Ty : !poly.tvar<@Ty> + poly.param @M : index + poly.expr @Sum { + %num = poly.read_const @Num : !poly.tvar<@Ty> + %cast = poly.unifiable_cast %num : (!poly.tvar<@Ty>) -> index + %m = poly.read_const @M : index + %sum = arith.addi %m, %cast : index + poly.yield %sum : index + } + function.def @value() -> index { + %sum = poly.read_const @Sum : index + function.return %sum : index + } + } + + function.def @main() -> index { + %value = function.call @InnerDirect::@value<[35, index, 7]>() : () -> index + function.return %value : index + } +} + +// CHECK-LABEL: module attributes {llzk.lang} { +// CHECK-NEXT: function.def @InnerDirect_35_i_7_value() -> index { +// CHECK-NEXT: %[[VAL_0:[0-9a-zA-Z_\.]+]] = arith.constant 42 : index +// CHECK-NEXT: function.return %[[VAL_0]] : index +// CHECK-NEXT: } +// CHECK-NEXT: function.def @main() -> index { +// CHECK-NEXT: %[[VAL_1:[0-9a-zA-Z_\.]+]] = function.call @InnerDirect_35_i_7_value() : () -> index +// CHECK-NEXT: function.return %[[VAL_1]] : index +// CHECK-NEXT: } +// CHECK-NEXT: } + +// ----- + +// A partially specialized expression is normalized when retained and can be evaluated when the +// remaining binding becomes concrete through the enclosing specialization. +module attributes {llzk.lang} { + poly.template @InnerPartial { + poly.param @Num + poly.param @Ty : !poly.tvar<@Ty> + poly.param @M : index + poly.expr @Sum { + %num = poly.read_const @Num : !poly.tvar<@Ty> + %cast = poly.unifiable_cast %num : (!poly.tvar<@Ty>) -> index + %m = poly.read_const @M : index + %sum = arith.addi %m, %cast : index + poly.yield %sum : index + } + function.def @value() -> index { + %sum = poly.read_const @Sum : index + function.return %sum : index + } + } + + poly.template @Bridge { + poly.param @M : index + function.def @value() -> index { + %sum = function.call @InnerPartial::@value<[35, index, @M]>() : () -> index + function.return %sum : index + } + } + + function.def @main() -> index { + %value = function.call @Bridge::@value<[7]>() : () -> index + function.return %value : index + } +} + +// CHECK-LABEL: module attributes {llzk.lang} { +// CHECK-NEXT: function.def @InnerPartial_35_i_7_value() -> index { +// CHECK-NEXT: %[[VAL_0:[0-9a-zA-Z_\.]+]] = arith.constant 42 : index +// CHECK-NEXT: function.return %[[VAL_0]] : index +// CHECK-NEXT: } +// CHECK-NEXT: function.def @Bridge_7_value() -> index { +// CHECK-NEXT: %[[VAL_1:[0-9a-zA-Z_\.]+]] = function.call @InnerPartial_35_i_7_value() : () -> index +// CHECK-NEXT: function.return %[[VAL_1]] : index +// CHECK-NEXT: } +// CHECK-NEXT: function.def @main() -> index { +// CHECK-NEXT: %[[VAL_2:[0-9a-zA-Z_\.]+]] = function.call @Bridge_7_value() : () -> index +// CHECK-NEXT: function.return %[[VAL_2]] : index +// CHECK-NEXT: } +// CHECK-NEXT: } + +// ----- + +// The same direct and deferred transitions are covered for struct specialization. +module attributes {llzk.lang, llzk.main = !struct.type<@Direct>} { + poly.template @InnerStruct { + poly.param @Num + poly.param @Ty : !poly.tvar<@Ty> + poly.param @M : index + poly.expr @Sum { + %num = poly.read_const @Num : !poly.tvar<@Ty> + %cast = poly.unifiable_cast %num : (!poly.tvar<@Ty>) -> index + %m = poly.read_const @M : index + %sum = arith.addi %m, %cast : index + poly.yield %sum : index + } + struct.def @Value { + struct.member @result : index + function.def @compute() -> !struct.type<@InnerStruct::@Value<[@Num, @Ty, @M]>> { + %sum = poly.read_const @Sum : index + %self = struct.new : <@InnerStruct::@Value<[@Num, @Ty, @M]>> + struct.writem %self[@result] = %sum : !struct.type<@InnerStruct::@Value<[@Num, @Ty, @M]>>, index + function.return %self : !struct.type<@InnerStruct::@Value<[@Num, @Ty, @M]>> + } + function.def @constrain(%self: !struct.type<@InnerStruct::@Value<[@Num, @Ty, @M]>>) { + function.return + } + } + } + + struct.def @Direct { + struct.member @value : !struct.type<@InnerStruct::@Value<[35, index, 7]>> + function.def @compute() -> !struct.type<@Direct> { + %self = struct.new : <@Direct> + function.return %self : !struct.type<@Direct> + } + function.def @constrain(%self: !struct.type<@Direct>) { + function.return + } + } +} + +// CHECK-LABEL: module attributes {llzk.lang, llzk.main = !struct.type<@Direct>} { +// CHECK-NEXT: struct.def @InnerStruct_35_i_7_Value { +// CHECK-NEXT: struct.member @result : index +// CHECK-NEXT: function.def @compute() -> !struct.type<@InnerStruct_35_i_7_Value> attributes {function.allow_witness} { +// CHECK-NEXT: %[[VAL_0:[0-9a-zA-Z_\.]+]] = arith.constant 42 : index +// CHECK-NEXT: %[[VAL_1:[0-9a-zA-Z_\.]+]] = struct.new : <@InnerStruct_35_i_7_Value> +// CHECK-NEXT: struct.writem %[[VAL_1]][@result] = %[[VAL_0]] : <@InnerStruct_35_i_7_Value>, index +// CHECK-NEXT: function.return %[[VAL_1]] : !struct.type<@InnerStruct_35_i_7_Value> +// CHECK-NEXT: } +// CHECK-NEXT: function.def @constrain(%[[VAL_2:[0-9a-zA-Z_\.]+]]: !struct.type<@InnerStruct_35_i_7_Value>) attributes {function.allow_constraint} { +// CHECK-NEXT: function.return +// CHECK-NEXT: } +// CHECK-NEXT: } +// CHECK-NEXT: struct.def @Direct { +// CHECK-NEXT: struct.member @value : !struct.type<@InnerStruct_35_i_7_Value> +// CHECK-NEXT: function.def @compute() -> !struct.type<@Direct> attributes {function.allow_witness} { +// CHECK-NEXT: %[[VAL_3:[0-9a-zA-Z_\.]+]] = struct.new : <@Direct> +// CHECK-NEXT: function.return %[[VAL_3]] : !struct.type<@Direct> +// CHECK-NEXT: } +// CHECK-NEXT: function.def @constrain(%[[VAL_4:[0-9a-zA-Z_\.]+]]: !struct.type<@Direct>) attributes {function.allow_constraint} { +// CHECK-NEXT: function.return +// CHECK-NEXT: } +// CHECK-NEXT: } +// CHECK-NEXT: } + +// ----- + +module attributes {llzk.lang, llzk.main = !struct.type<@Main>} { + poly.template @InnerStructDeferred { + poly.param @Num + poly.param @Ty : !poly.tvar<@Ty> + poly.param @M : index + poly.expr @Sum { + %num = poly.read_const @Num : !poly.tvar<@Ty> + %cast = poly.unifiable_cast %num : (!poly.tvar<@Ty>) -> index + %m = poly.read_const @M : index + %sum = arith.addi %m, %cast : index + poly.yield %sum : index + } + struct.def @Value { + struct.member @result : index + function.def @compute() -> !struct.type<@InnerStructDeferred::@Value<[@Num, @Ty, @M]>> { + %sum = poly.read_const @Sum : index + %self = struct.new : <@InnerStructDeferred::@Value<[@Num, @Ty, @M]>> + struct.writem %self[@result] = %sum : !struct.type<@InnerStructDeferred::@Value<[@Num, @Ty, @M]>>, index + function.return %self : !struct.type<@InnerStructDeferred::@Value<[@Num, @Ty, @M]>> + } + function.def @constrain(%self: !struct.type<@InnerStructDeferred::@Value<[@Num, @Ty, @M]>>) { + function.return + } + } + } + + poly.template @StructBridge { + poly.param @M : index + struct.def @Wrapper { + struct.member @value : !struct.type<@InnerStructDeferred::@Value<[35, index, @M]>> + function.def @compute() -> !struct.type<@StructBridge::@Wrapper<[@M]>> { + %self = struct.new : <@StructBridge::@Wrapper<[@M]>> + function.return %self : !struct.type<@StructBridge::@Wrapper<[@M]>> + } + function.def @constrain(%self: !struct.type<@StructBridge::@Wrapper<[@M]>>) { + function.return + } + } + } + + struct.def @Main { + struct.member @value : !struct.type<@StructBridge::@Wrapper<[7]>> + function.def @compute() -> !struct.type<@Main> { + %self = struct.new : <@Main> + function.return %self : !struct.type<@Main> + } + function.def @constrain(%self: !struct.type<@Main>) { + function.return + } + } +} + +// CHECK-LABEL: module attributes {llzk.lang, llzk.main = !struct.type<@Main>} { +// CHECK-NEXT: poly.template @"InnerStructDeferred_35_i_\1A" { +// CHECK-NEXT: poly.param @M : index +// CHECK-NEXT: poly.expr @Sum { +// CHECK-NEXT: %[[VAL_0:[0-9a-zA-Z_\.]+]] = arith.constant 35 : index +// CHECK-NEXT: %[[VAL_1:[0-9a-zA-Z_\.]+]] = poly.read_const @M : index +// CHECK-NEXT: %[[VAL_2:[0-9a-zA-Z_\.]+]] = arith.addi %[[VAL_1]], %[[VAL_0]] : index +// CHECK-NEXT: poly.yield %[[VAL_2]] : index +// CHECK-NEXT: } +// CHECK-NEXT: struct.def @Value { +// CHECK-NEXT: struct.member @result : index +// CHECK-NEXT: function.def @compute() -> !struct.type<@"InnerStructDeferred_35_i_\1A"::@Value<[@M]>> attributes {function.allow_witness} { +// CHECK-NEXT: %[[VAL_3:[0-9a-zA-Z_\.]+]] = poly.read_const @Sum : index +// CHECK-NEXT: %[[VAL_4:[0-9a-zA-Z_\.]+]] = struct.new : <@"InnerStructDeferred_35_i_\1A"::@Value<[@M]>> +// CHECK-NEXT: struct.writem %[[VAL_4]][@result] = %[[VAL_3]] : <@"InnerStructDeferred_35_i_\1A"::@Value<[@M]>>, index +// CHECK-NEXT: function.return %[[VAL_4]] : !struct.type<@"InnerStructDeferred_35_i_\1A"::@Value<[@M]>> +// CHECK-NEXT: } +// CHECK-NEXT: function.def @constrain(%[[VAL_5:[0-9a-zA-Z_\.]+]]: !struct.type<@"InnerStructDeferred_35_i_\1A"::@Value<[@M]>>) attributes {function.allow_constraint} { +// CHECK-NEXT: function.return +// CHECK-NEXT: } +// CHECK-NEXT: } +// CHECK-NEXT: } {poly.name_pattern = ["InnerStructDeferred_35_i_", ""]} +// CHECK-NEXT: struct.def @InnerStructDeferred_35_i_7_Value { +// CHECK-NEXT: struct.member @result : index +// CHECK-NEXT: function.def @compute() -> !struct.type<@InnerStructDeferred_35_i_7_Value> attributes {function.allow_witness} { +// CHECK-NEXT: %[[VAL_6:[0-9a-zA-Z_\.]+]] = arith.constant 42 : index +// CHECK-NEXT: %[[VAL_7:[0-9a-zA-Z_\.]+]] = struct.new : <@InnerStructDeferred_35_i_7_Value> +// CHECK-NEXT: struct.writem %[[VAL_7]][@result] = %[[VAL_6]] : <@InnerStructDeferred_35_i_7_Value>, index +// CHECK-NEXT: function.return %[[VAL_7]] : !struct.type<@InnerStructDeferred_35_i_7_Value> +// CHECK-NEXT: } +// CHECK-NEXT: function.def @constrain(%[[VAL_8:[0-9a-zA-Z_\.]+]]: !struct.type<@InnerStructDeferred_35_i_7_Value>) attributes {function.allow_constraint} { +// CHECK-NEXT: function.return +// CHECK-NEXT: } +// CHECK-NEXT: } +// CHECK-NEXT: struct.def @StructBridge_7_Wrapper { +// CHECK-NEXT: struct.member @value : !struct.type<@InnerStructDeferred_35_i_7_Value> +// CHECK-NEXT: function.def @compute() -> !struct.type<@StructBridge_7_Wrapper> attributes {function.allow_witness} { +// CHECK-NEXT: %[[VAL_9:[0-9a-zA-Z_\.]+]] = struct.new : <@StructBridge_7_Wrapper> +// CHECK-NEXT: function.return %[[VAL_9]] : !struct.type<@StructBridge_7_Wrapper> +// CHECK-NEXT: } +// CHECK-NEXT: function.def @constrain(%[[VAL_10:[0-9a-zA-Z_\.]+]]: !struct.type<@StructBridge_7_Wrapper>) attributes {function.allow_constraint} { +// CHECK-NEXT: function.return +// CHECK-NEXT: } +// CHECK-NEXT: } +// CHECK-NEXT: struct.def @Main { +// CHECK-NEXT: struct.member @value : !struct.type<@StructBridge_7_Wrapper> +// CHECK-NEXT: function.def @compute() -> !struct.type<@Main> attributes {function.allow_witness} { +// CHECK-NEXT: %[[VAL_11:[0-9a-zA-Z_\.]+]] = struct.new : <@Main> +// CHECK-NEXT: function.return %[[VAL_11]] : !struct.type<@Main> +// CHECK-NEXT: } +// CHECK-NEXT: function.def @constrain(%[[VAL_12:[0-9a-zA-Z_\.]+]]: !struct.type<@Main>) attributes {function.allow_constraint} { +// CHECK-NEXT: function.return +// CHECK-NEXT: } +// CHECK-NEXT: } +// CHECK-NEXT: } diff --git a/test/Transforms/Flattening/instantiate_func_felt_normalization.llzk b/test/Transforms/Flattening/instantiate_func_felt_normalization.llzk new file mode 100644 index 0000000000..29bcbf6d2e --- /dev/null +++ b/test/Transforms/Flattening/instantiate_func_felt_normalization.llzk @@ -0,0 +1,280 @@ +// RUN: llzk-opt -split-input-file -llzk-flatten %s | FileCheck %s + +module attributes {llzk.lang} { + poly.template @CanonicalFelt { + poly.param @F : !felt.type<"bn128"> + + function.def @value() -> !felt.type<"bn128"> { + %value = poly.read_const @F : !felt.type<"bn128"> + function.return %value : !felt.type<"bn128"> + } + } + + function.def @main() -> !felt.type<"bn128"> { + %fieldless = function.call @CanonicalFelt::@value<[#felt]>() : + () -> !felt.type<"bn128"> + %integer = function.call @CanonicalFelt::@value<[35]>() : () -> !felt.type<"bn128"> + %fielded = function.call + @CanonicalFelt::@value<[#felt>]>() : + () -> !felt.type<"bn128"> + %other = function.call @CanonicalFelt::@value<[36]>() : () -> !felt.type<"bn128"> + function.return %fielded : !felt.type<"bn128"> + } +} + +// CHECK-LABEL: module attributes {llzk.lang} { +// CHECK-NEXT: function.def @"CanonicalFelt_f<35:5:bn128>_value"() -> !felt.type<"bn128"> { +// CHECK-NEXT: %[[VAL_0:[0-9a-zA-Z_\.]+]] = felt.const 35 : <"bn128"> +// CHECK-NEXT: function.return %[[VAL_0]] : !felt.type<"bn128"> +// CHECK-NEXT: } +// CHECK-NEXT: function.def @"CanonicalFelt_f<36:5:bn128>_value"() -> !felt.type<"bn128"> { +// CHECK-NEXT: %[[VAL_1:[0-9a-zA-Z_\.]+]] = felt.const 36 : <"bn128"> +// CHECK-NEXT: function.return %[[VAL_1]] : !felt.type<"bn128"> +// CHECK-NEXT: } +// CHECK-NEXT: function.def @main() -> !felt.type<"bn128"> { +// CHECK-NEXT: %[[VAL_2:[0-9a-zA-Z_\.]+]] = function.call @"CanonicalFelt_f<35:5:bn128>_value"() : () -> !felt.type<"bn128"> +// CHECK-NEXT: %[[VAL_3:[0-9a-zA-Z_\.]+]] = function.call @"CanonicalFelt_f<35:5:bn128>_value"() : () -> !felt.type<"bn128"> +// CHECK-NEXT: %[[VAL_4:[0-9a-zA-Z_\.]+]] = function.call @"CanonicalFelt_f<35:5:bn128>_value"() : () -> !felt.type<"bn128"> +// CHECK-NEXT: %[[VAL_5:[0-9a-zA-Z_\.]+]] = function.call @"CanonicalFelt_f<36:5:bn128>_value"() : () -> !felt.type<"bn128"> +// CHECK-NEXT: function.return %[[VAL_4]] : !felt.type<"bn128"> +// CHECK-NEXT: } +// CHECK-NEXT: } + +// ----- + +// Equivalent felt spellings reuse one partial function specialization before the remaining +// parameter is completed, then all three routes call the same fully specialized function. +module attributes {llzk.lang} { + poly.template @CanonicalPartialFunction { + poly.param @F : !felt.type<"bn128"> + poly.param @M : index + + function.def @value() -> !felt.type<"bn128"> { + %value = poly.read_const @F : !felt.type<"bn128"> + function.return %value : !felt.type<"bn128"> + } + } + + poly.template @FunctionBridge { + poly.param @M : index + + function.def @value() -> !felt.type<"bn128"> { + %fieldless = function.call + @CanonicalPartialFunction::@value<[#felt, @M]>() : + () -> !felt.type<"bn128"> + %integer = function.call @CanonicalPartialFunction::@value<[35, @M]>() : + () -> !felt.type<"bn128"> + %fielded = function.call @CanonicalPartialFunction::@value<[ + #felt>, @M + ]>() : () -> !felt.type<"bn128"> + function.return %fielded : !felt.type<"bn128"> + } + } + + function.def @main() -> !felt.type<"bn128"> { + %value = function.call @FunctionBridge::@value<[7]>() : () -> !felt.type<"bn128"> + function.return %value : !felt.type<"bn128"> + } +} + +// CHECK-LABEL: module attributes {llzk.lang} { +// CHECK-NEXT: function.def @"CanonicalPartialFunction_f<35:5:bn128>_7_value"() -> !felt.type<"bn128"> { +// CHECK-NEXT: %[[VAL_0:[0-9a-zA-Z_\.]+]] = felt.const 35 : <"bn128"> +// CHECK-NEXT: function.return %[[VAL_0]] : !felt.type<"bn128"> +// CHECK-NEXT: } +// CHECK-NEXT: function.def @FunctionBridge_7_value() -> !felt.type<"bn128"> { +// CHECK-NEXT: %[[VAL_1:[0-9a-zA-Z_\.]+]] = function.call @"CanonicalPartialFunction_f<35:5:bn128>_7_value"() : () -> !felt.type<"bn128"> +// CHECK-NEXT: %[[VAL_2:[0-9a-zA-Z_\.]+]] = function.call @"CanonicalPartialFunction_f<35:5:bn128>_7_value"() : () -> !felt.type<"bn128"> +// CHECK-NEXT: %[[VAL_3:[0-9a-zA-Z_\.]+]] = function.call @"CanonicalPartialFunction_f<35:5:bn128>_7_value"() : () -> !felt.type<"bn128"> +// CHECK-NEXT: function.return %[[VAL_3]] : !felt.type<"bn128"> +// CHECK-NEXT: } +// CHECK-NEXT: function.def @main() -> !felt.type<"bn128"> { +// CHECK-NEXT: %[[VAL_4:[0-9a-zA-Z_\.]+]] = function.call @FunctionBridge_7_value() : () -> !felt.type<"bn128"> +// CHECK-NEXT: function.return %[[VAL_4]] : !felt.type<"bn128"> +// CHECK-NEXT: } +// CHECK-NEXT: } + +// ----- + +// Equivalent felt spellings reuse one partial struct specialization before the enclosing +// parameter is completed, then all three members use the same fully specialized struct. +module attributes { + llzk.lang, + llzk.main = !struct.type<@StructBridge::@Wrapper<[7]>> +} { + poly.template @CanonicalPartialStruct { + poly.param @F : !felt.type<"bn128"> + poly.param @M : index + + struct.def @Value { + struct.member @result : !felt.type<"bn128"> + + function.def @compute() -> !struct.type<@CanonicalPartialStruct::@Value<[@F, @M]>> { + %value = poly.read_const @F : !felt.type<"bn128"> + %self = struct.new : !struct.type<@CanonicalPartialStruct::@Value<[@F, @M]>> + struct.writem %self[@result] = %value : + !struct.type<@CanonicalPartialStruct::@Value<[@F, @M]>>, !felt.type<"bn128"> + function.return %self : !struct.type<@CanonicalPartialStruct::@Value<[@F, @M]>> + } + + function.def @constrain( + %self: !struct.type<@CanonicalPartialStruct::@Value<[@F, @M]>> + ) { + function.return + } + } + } + + poly.template @StructBridge { + poly.param @M : index + + struct.def @Wrapper { + struct.member @fieldless : + !struct.type<@CanonicalPartialStruct::@Value<[#felt, @M]>> + struct.member @integer : !struct.type<@CanonicalPartialStruct::@Value<[35, @M]>> + struct.member @fielded : !struct.type<@CanonicalPartialStruct::@Value<[ + #felt>, @M + ]>> + + function.def @compute() -> !struct.type<@StructBridge::@Wrapper<[@M]>> { + %self = struct.new : !struct.type<@StructBridge::@Wrapper<[@M]>> + function.return %self : !struct.type<@StructBridge::@Wrapper<[@M]>> + } + + function.def @constrain(%self: !struct.type<@StructBridge::@Wrapper<[@M]>>) { + function.return + } + } + } +} + +// CHECK-LABEL: module attributes {llzk.lang, llzk.main = !struct.type<@StructBridge_7_Wrapper>} { +// CHECK-NEXT: struct.def @"CanonicalPartialStruct_f<35:5:bn128>_7_Value" { +// CHECK-NEXT: struct.member @result : !felt.type<"bn128"> +// CHECK-NEXT: function.def @compute() -> !struct.type<@"CanonicalPartialStruct_f<35:5:bn128>_7_Value"> attributes {function.allow_witness} { +// CHECK-NEXT: %[[VAL_0:[0-9a-zA-Z_\.]+]] = felt.const 35 : <"bn128"> +// CHECK-NEXT: %[[VAL_1:[0-9a-zA-Z_\.]+]] = struct.new : <@"CanonicalPartialStruct_f<35:5:bn128>_7_Value"> +// CHECK-NEXT: struct.writem %[[VAL_1]][@result] = %[[VAL_0]] : <@"CanonicalPartialStruct_f<35:5:bn128>_7_Value">, !felt.type<"bn128"> +// CHECK-NEXT: function.return %[[VAL_1]] : !struct.type<@"CanonicalPartialStruct_f<35:5:bn128>_7_Value"> +// CHECK-NEXT: } +// CHECK-NEXT: function.def @constrain(%[[VAL_2:[0-9a-zA-Z_\.]+]]: !struct.type<@"CanonicalPartialStruct_f<35:5:bn128>_7_Value">) attributes {function.allow_constraint} { +// CHECK-NEXT: function.return +// CHECK-NEXT: } +// CHECK-NEXT: } +// CHECK-NEXT: poly.template @"CanonicalPartialStruct_f<35:5:bn128>_\1A" { +// CHECK-NEXT: poly.param @M : index +// CHECK-NEXT: struct.def @Value { +// CHECK-NEXT: struct.member @result : !felt.type<"bn128"> +// CHECK-NEXT: function.def @compute() -> !struct.type<@"CanonicalPartialStruct_f<35:5:bn128>_\1A"::@Value<[@M]>> attributes {function.allow_witness} { +// CHECK-NEXT: %[[VAL_3:[0-9a-zA-Z_\.]+]] = felt.const 35 : <"bn128"> +// CHECK-NEXT: %[[VAL_4:[0-9a-zA-Z_\.]+]] = struct.new : <@"CanonicalPartialStruct_f<35:5:bn128>_\1A"::@Value<[@M]>> +// CHECK-NEXT: struct.writem %[[VAL_4]][@result] = %[[VAL_3]] : <@"CanonicalPartialStruct_f<35:5:bn128>_\1A"::@Value<[@M]>>, !felt.type<"bn128"> +// CHECK-NEXT: function.return %[[VAL_4]] : !struct.type<@"CanonicalPartialStruct_f<35:5:bn128>_\1A"::@Value<[@M]>> +// CHECK-NEXT: } +// CHECK-NEXT: function.def @constrain(%[[VAL_5:[0-9a-zA-Z_\.]+]]: !struct.type<@"CanonicalPartialStruct_f<35:5:bn128>_\1A"::@Value<[@M]>>) attributes {function.allow_constraint} { +// CHECK-NEXT: function.return +// CHECK-NEXT: } +// CHECK-NEXT: } +// CHECK-NEXT: } {poly.name_pattern = ["CanonicalPartialStruct_f<35:5:bn128>_", ""]} +// CHECK-NEXT: struct.def @StructBridge_7_Wrapper { +// CHECK-NEXT: struct.member @fieldless : !struct.type<@"CanonicalPartialStruct_f<35:5:bn128>_7_Value"> +// CHECK-NEXT: struct.member @integer : !struct.type<@"CanonicalPartialStruct_f<35:5:bn128>_7_Value"> +// CHECK-NEXT: struct.member @fielded : !struct.type<@"CanonicalPartialStruct_f<35:5:bn128>_7_Value"> +// CHECK-NEXT: function.def @compute() -> !struct.type<@StructBridge_7_Wrapper> attributes {function.allow_witness} { +// CHECK-NEXT: %[[VAL_6:[0-9a-zA-Z_\.]+]] = struct.new : <@StructBridge_7_Wrapper> +// CHECK-NEXT: function.return %[[VAL_6]] : !struct.type<@StructBridge_7_Wrapper> +// CHECK-NEXT: } +// CHECK-NEXT: function.def @constrain(%[[VAL_7:[0-9a-zA-Z_\.]+]]: !struct.type<@StructBridge_7_Wrapper>) attributes {function.allow_constraint} { +// CHECK-NEXT: function.return +// CHECK-NEXT: } +// CHECK-NEXT: } +// CHECK-NEXT: } + +// ----- + +module attributes { + llzk.lang, + llzk.main = !struct.type<@Main> +} { + poly.template @CanonicalFeltStruct { + poly.param @F : !felt.type<"bn128"> + + struct.def @Value { + struct.member @result : !felt.type<"bn128"> + + function.def @compute() -> !struct.type<@CanonicalFeltStruct::@Value<[@F]>> { + %value = poly.read_const @F : !felt.type<"bn128"> + %self = struct.new : !struct.type<@CanonicalFeltStruct::@Value<[@F]>> + struct.writem %self[@result] = %value : + !struct.type<@CanonicalFeltStruct::@Value<[@F]>>, !felt.type<"bn128"> + function.return %self : !struct.type<@CanonicalFeltStruct::@Value<[@F]>> + } + + function.def @constrain( + %self: !struct.type<@CanonicalFeltStruct::@Value<[@F]>> + ) { + function.return + } + } + } + + struct.def @Main { + struct.member @fieldless : + !struct.type<@CanonicalFeltStruct::@Value<[#felt]>> + struct.member @integer : !struct.type<@CanonicalFeltStruct::@Value<[35]>> + struct.member @fielded : + !struct.type<@CanonicalFeltStruct::@Value<[ + #felt> + ]>> + struct.member @other : !struct.type<@CanonicalFeltStruct::@Value<[36]>> + + function.def @compute() -> !struct.type<@Main> { + %self = struct.new : !struct.type<@Main> + function.return %self : !struct.type<@Main> + } + + function.def @constrain(%self: !struct.type<@Main>) { + function.return + } + } +} + +// CHECK-LABEL: module attributes {llzk.lang, llzk.main = !struct.type<@Main>} { +// CHECK-NEXT: struct.def @"CanonicalFeltStruct_f<35:5:bn128>_Value" { +// CHECK-NEXT: struct.member @result : !felt.type<"bn128"> +// CHECK-NEXT: function.def @compute() -> !struct.type<@"CanonicalFeltStruct_f<35:5:bn128>_Value"> attributes {function.allow_witness} { +// CHECK-NEXT: %[[VAL_0:[0-9a-zA-Z_\.]+]] = felt.const 35 : <"bn128"> +// CHECK-NEXT: %[[VAL_1:[0-9a-zA-Z_\.]+]] = struct.new : <@"CanonicalFeltStruct_f<35:5:bn128>_Value"> +// CHECK-NEXT: struct.writem %[[VAL_1]][@result] = %[[VAL_0]] : <@"CanonicalFeltStruct_f<35:5:bn128>_Value">, !felt.type<"bn128"> +// CHECK-NEXT: function.return %[[VAL_1]] : !struct.type<@"CanonicalFeltStruct_f<35:5:bn128>_Value"> +// CHECK-NEXT: } +// CHECK-NEXT: function.def @constrain(%[[VAL_2:[0-9a-zA-Z_\.]+]]: !struct.type<@"CanonicalFeltStruct_f<35:5:bn128>_Value">) attributes {function.allow_constraint} { +// CHECK-NEXT: function.return +// CHECK-NEXT: } +// CHECK-NEXT: } +// CHECK-NEXT: struct.def @"CanonicalFeltStruct_f<36:5:bn128>_Value" { +// CHECK-NEXT: struct.member @result : !felt.type<"bn128"> +// CHECK-NEXT: function.def @compute() -> !struct.type<@"CanonicalFeltStruct_f<36:5:bn128>_Value"> attributes {function.allow_witness} { +// CHECK-NEXT: %[[VAL_3:[0-9a-zA-Z_\.]+]] = felt.const 36 : <"bn128"> +// CHECK-NEXT: %[[VAL_4:[0-9a-zA-Z_\.]+]] = struct.new : <@"CanonicalFeltStruct_f<36:5:bn128>_Value"> +// CHECK-NEXT: struct.writem %[[VAL_4]][@result] = %[[VAL_3]] : <@"CanonicalFeltStruct_f<36:5:bn128>_Value">, !felt.type<"bn128"> +// CHECK-NEXT: function.return %[[VAL_4]] : !struct.type<@"CanonicalFeltStruct_f<36:5:bn128>_Value"> +// CHECK-NEXT: } +// CHECK-NEXT: function.def @constrain(%[[VAL_5:[0-9a-zA-Z_\.]+]]: !struct.type<@"CanonicalFeltStruct_f<36:5:bn128>_Value">) attributes {function.allow_constraint} { +// CHECK-NEXT: function.return +// CHECK-NEXT: } +// CHECK-NEXT: } +// CHECK-NEXT: struct.def @Main { +// CHECK-NEXT: struct.member @fieldless : !struct.type<@"CanonicalFeltStruct_f<35:5:bn128>_Value"> +// CHECK-NEXT: struct.member @integer : !struct.type<@"CanonicalFeltStruct_f<35:5:bn128>_Value"> +// CHECK-NEXT: struct.member @fielded : !struct.type<@"CanonicalFeltStruct_f<35:5:bn128>_Value"> +// CHECK-NEXT: struct.member @other : !struct.type<@"CanonicalFeltStruct_f<36:5:bn128>_Value"> +// CHECK-NEXT: function.def @compute() -> !struct.type<@Main> attributes {function.allow_witness} { +// CHECK-NEXT: %[[VAL_6:[0-9a-zA-Z_\.]+]] = struct.new : <@Main> +// CHECK-NEXT: function.return %[[VAL_6]] : !struct.type<@Main> +// CHECK-NEXT: } +// CHECK-NEXT: function.def @constrain(%[[VAL_7:[0-9a-zA-Z_\.]+]]: !struct.type<@Main>) attributes {function.allow_constraint} { +// CHECK-NEXT: function.return +// CHECK-NEXT: } +// CHECK-NEXT: } +// CHECK-NEXT: } diff --git a/test/Transforms/Flattening/instantiate_func_felt_normalization_fail.llzk b/test/Transforms/Flattening/instantiate_func_felt_normalization_fail.llzk new file mode 100644 index 0000000000..995175d990 --- /dev/null +++ b/test/Transforms/Flattening/instantiate_func_felt_normalization_fail.llzk @@ -0,0 +1,20 @@ +// RUN: llzk-opt -llzk-flatten -verify-diagnostics %s + +module attributes {llzk.lang} { + poly.template @CanonicalFelt { + poly.param @F : !felt.type<"bn128"> + + function.def @value() -> !felt.type<"bn128"> { + %value = poly.read_const @F : !felt.type<"bn128"> + function.return %value : !felt.type<"bn128"> + } + } + + function.def @main() -> !felt.type<"bn128"> { + // expected-error@+1 {{'function.call' op instantiation value '#felt> : !felt.type<"goldilocks">' is not compatible with parameter "@F" type restriction '!felt.type<"bn128">'}} + %value = function.call + @CanonicalFelt::@value<[#felt>]>() : + () -> !felt.type<"bn128"> + function.return %value : !felt.type<"bn128"> + } +} diff --git a/test/Transforms/Flattening/instantiate_func_rollback.llzk b/test/Transforms/Flattening/instantiate_func_rollback.llzk new file mode 100644 index 0000000000..7cddc54e01 --- /dev/null +++ b/test/Transforms/Flattening/instantiate_func_rollback.llzk @@ -0,0 +1,268 @@ +// RUN: llzk-opt -llzk-flatten=cleanup=disabled -verify-diagnostics -mlir-print-ir-after-failure %s 2>&1 | FileCheck --enable-var-scope %s + +// Failed full and partial specializations must retain neither their cloned owners, symbol-table +// entries, nor delayed diagnostics. Later specializations that request the same preferred names +// must still use those names successfully in the same pass run. +module attributes {llzk.lang} { + function.def @full_rollback_inner( + %arg: !array.type<8,9 x !felt.type<"bn128">> + ) -> !felt.type<"bn128"> { + %zero = arith.constant 0 : index + %value = array.read %arg[%zero, %zero] : + !array.type<8,9 x !felt.type<"bn128">>, !felt.type<"bn128"> + function.return %value : !felt.type<"bn128"> + } + + poly.template @FullRollbackSource { + poly.param @N : index + poly.param @M : index + poly.param @T : !poly.tvar<@T> + poly.param @B + + function.def @apply(%arg: !array.type<@N,@M x !poly.tvar<@T>>) -> !poly.tvar<@T> { + // Converting this read queues a warning that rollback must discard. + %flag = poly.read_const @B : i1 + // expected-error@+2 {{'function.call' op operand type mismatch: expected type '!array.type<8,9 x !felt.type<"bn128">>', but found '!array.type<8,9 x i1>' for operand number 0}} + // expected-error@+1 {{'function.call' op result type mismatch: expected type '!felt.type<"bn128">', but found 'i1' for result number 0}} + %value = function.call @full_rollback_inner(%arg) : + (!array.type<@N,@M x !poly.tvar<@T>>) -> !poly.tvar<@T> + function.return %value : !poly.tvar<@T> + } + } + + function.def @call_full_rollback(%arg: !array.type<8,9 x i1>) -> i1 { + // expected-error@+1 {{failure while creating instantiated function 'FullRollbackSource_8_9_b_2_apply'}} + %value = function.call @FullRollbackSource::@apply<[8, 9, i1, 2]>(%arg) : + (!array.type<8,9 x i1>) -> i1 + function.return %value : i1 + } + + // This distinct source renders the same preferred full-specialization name as the failed clone. + poly.template @FullRollbackSource_8_9_b { + poly.param @B : index + + function.def @apply() -> index { + %value = poly.read_const @B : index + function.return %value : index + } + } + + function.def @call_full_after_rollback() -> index { + %value = function.call @FullRollbackSource_8_9_b::@apply<[2]>() : () -> index + function.return %value : index + } + + function.def @partial_rollback_inner( + %arg: !array.type<8,9 x !felt.type<"bn128">> + ) -> !felt.type<"bn128"> { + %zero = arith.constant 0 : index + %value = array.read %arg[%zero, %zero] : + !array.type<8,9 x !felt.type<"bn128">>, !felt.type<"bn128"> + function.return %value : !felt.type<"bn128"> + } + + poly.template @PartialRollbackSource { + poly.param @N : index + poly.param @M : index + poly.param @T : !poly.tvar<@T> + poly.param @B + + function.def @apply(%arg: !array.type<@N,@M x !poly.tvar<@T>>) -> !poly.tvar<@T> { + // Converting this read queues a warning that rollback must discard. + %flag = poly.read_const @B : i1 + // expected-error@+2 {{'function.call' op operand type mismatch: expected type '!array.type<8,9 x !felt.type<"bn128">>', but found '!array.type<8,@M x i1>' for operand number 0}} + // expected-error@+1 {{'function.call' op result type mismatch: expected type '!felt.type<"bn128">', but found 'i1' for result number 0}} + %value = function.call @partial_rollback_inner(%arg) : + (!array.type<@N,@M x !poly.tvar<@T>>) -> !poly.tvar<@T> + function.return %value : !poly.tvar<@T> + } + } + + poly.template @PartialRollbackCaller { + poly.param @M : index + + function.def @apply(%arg: !array.type<8,@M x i1>) -> i1 { + // expected-error@+1 {{failure while creating instantiated function 'apply'}} + %value = function.call @PartialRollbackSource::@apply<[8, @M, i1, 2]>(%arg) : + (!array.type<8,@M x i1>) -> i1 + function.return %value : i1 + } + } + + // This distinct source renders the same preferred reduced-template name as the failed clone. + poly.template @PartialRollbackSource_8 { + poly.param @M : index + poly.param @T : !poly.tvar<@T> + poly.param @B + + function.def @apply() -> index { + %value = poly.read_const @B : index + function.return %value : index + } + } + + poly.template @PartialRollbackCollisionCaller { + poly.param @M : index + + function.def @apply() -> index { + %value = function.call @PartialRollbackSource_8::@apply<[@M, i1, 2]>() : () -> index + function.return %value : index + } + } + + poly.template @GoodRollbackSource { + poly.param @N : index + poly.param @M : index + poly.param @T : !poly.tvar<@T> + poly.param @B + + function.def @apply(%arg: !array.type<@N,@M x !poly.tvar<@T>>) -> !poly.tvar<@T> { + %flag = poly.read_const @B : i1 + %zero = arith.constant 0 : index + %value = array.read %arg[%zero, %zero] : + !array.type<@N,@M x !poly.tvar<@T>>, !poly.tvar<@T> + function.return %value : !poly.tvar<@T> + } + } + + poly.template @GoodRollbackCaller { + poly.param @M : index + + function.def @apply( + %arg: !array.type<8,@M x !felt.type<"bn128">> + ) -> !felt.type<"bn128"> { + %value = function.call @GoodRollbackSource::@apply<[ + 8, @M, !felt.type<"bn128">, 1 + ]>(%arg) : (!array.type<8,@M x !felt.type<"bn128">>) -> !felt.type<"bn128"> + function.return %value : !felt.type<"bn128"> + } + } + + function.def @call_good_after_rollback( + %arg: !array.type<8,9 x !felt.type<"bn128">> + ) -> !felt.type<"bn128"> { + %value = function.call @GoodRollbackCaller::@apply<[9]>(%arg) : + (!array.type<8,9 x !felt.type<"bn128">>) -> !felt.type<"bn128"> + function.return %value : !felt.type<"bn128"> + } +} + +// CHECK-LABEL: module attributes {llzk.lang} { +// CHECK-NEXT: function.def @full_rollback_inner(%[[VAL_0:[0-9a-zA-Z_\.]+]]: !array.type<8,9 x !felt.type<"bn128">>) -> !felt.type<"bn128"> { +// CHECK-NEXT: %[[VAL_1:[0-9a-zA-Z_\.]+]] = arith.constant 0 : index +// CHECK-NEXT: %[[VAL_2:[0-9a-zA-Z_\.]+]] = array.read %[[VAL_0]]{{\[}}%[[VAL_1]], %[[VAL_1]]] : <8,9 x !felt.type<"bn128">>, !felt.type<"bn128"> +// CHECK-NEXT: function.return %[[VAL_2]] : !felt.type<"bn128"> +// CHECK-NEXT: } +// CHECK-NEXT: poly.template @FullRollbackSource { +// CHECK-NEXT: poly.param @N : index +// CHECK-NEXT: poly.param @M : index +// CHECK-NEXT: poly.param @T : !poly.tvar<@T> +// CHECK-NEXT: poly.param @B +// CHECK-NEXT: function.def @apply(%[[VAL_3:[0-9a-zA-Z_\.]+]]: !array.type<@N,@M x !poly.tvar<@T>>) -> !poly.tvar<@T> { +// CHECK-NEXT: %[[VAL_4:[0-9a-zA-Z_\.]+]] = poly.read_const @B : i1 +// CHECK-NEXT: %[[VAL_5:[0-9a-zA-Z_\.]+]] = function.call @full_rollback_inner(%[[VAL_3]]) : (!array.type<@N,@M x !poly.tvar<@T>>) -> !poly.tvar<@T> +// CHECK-NEXT: function.return %[[VAL_5]] : !poly.tvar<@T> +// CHECK-NEXT: } +// CHECK-NEXT: } +// CHECK-NEXT: function.def @call_full_rollback(%[[VAL_6:[0-9a-zA-Z_\.]+]]: !array.type<8,9 x i1>) -> i1 { +// CHECK-NEXT: %[[VAL_7:[0-9a-zA-Z_\.]+]] = function.call @FullRollbackSource::@apply<[8, 9, i1, 2]>(%[[VAL_6]]) : (!array.type<8,9 x i1>) -> i1 +// CHECK-NEXT: function.return %[[VAL_7]] : i1 +// CHECK-NEXT: } +// CHECK-NEXT: function.def @FullRollbackSource_8_9_b_2_apply() -> index { +// CHECK-NEXT: %[[VAL_8:[0-9a-zA-Z_\.]+]] = arith.constant 2 : index +// CHECK-NEXT: function.return %[[VAL_8]] : index +// CHECK-NEXT: } +// CHECK-NEXT: poly.template @FullRollbackSource_8_9_b { +// CHECK-NEXT: poly.param @B : index +// CHECK-NEXT: function.def @apply() -> index { +// CHECK-NEXT: %[[VAL_9:[0-9a-zA-Z_\.]+]] = poly.read_const @B : index +// CHECK-NEXT: function.return %[[VAL_9]] : index +// CHECK-NEXT: } +// CHECK-NEXT: } +// CHECK-NEXT: function.def @call_full_after_rollback() -> index { +// CHECK-NEXT: %[[VAL_10:[0-9a-zA-Z_\.]+]] = function.call @FullRollbackSource_8_9_b_2_apply() : () -> index +// CHECK-NEXT: function.return %[[VAL_10]] : index +// CHECK-NEXT: } +// CHECK-NEXT: function.def @partial_rollback_inner(%[[VAL_11:[0-9a-zA-Z_\.]+]]: !array.type<8,9 x !felt.type<"bn128">>) -> !felt.type<"bn128"> { +// CHECK-NEXT: %[[VAL_12:[0-9a-zA-Z_\.]+]] = arith.constant 0 : index +// CHECK-NEXT: %[[VAL_13:[0-9a-zA-Z_\.]+]] = array.read %[[VAL_11]]{{\[}}%[[VAL_12]], %[[VAL_12]]] : <8,9 x !felt.type<"bn128">>, !felt.type<"bn128"> +// CHECK-NEXT: function.return %[[VAL_13]] : !felt.type<"bn128"> +// CHECK-NEXT: } +// CHECK-NEXT: poly.template @PartialRollbackSource { +// CHECK-NEXT: poly.param @N : index +// CHECK-NEXT: poly.param @M : index +// CHECK-NEXT: poly.param @T : !poly.tvar<@T> +// CHECK-NEXT: poly.param @B +// CHECK-NEXT: function.def @apply(%[[VAL_14:[0-9a-zA-Z_\.]+]]: !array.type<@N,@M x !poly.tvar<@T>>) -> !poly.tvar<@T> { +// CHECK-NEXT: %[[VAL_15:[0-9a-zA-Z_\.]+]] = poly.read_const @B : i1 +// CHECK-NEXT: %[[VAL_16:[0-9a-zA-Z_\.]+]] = function.call @partial_rollback_inner(%[[VAL_14]]) : (!array.type<@N,@M x !poly.tvar<@T>>) -> !poly.tvar<@T> +// CHECK-NEXT: function.return %[[VAL_16]] : !poly.tvar<@T> +// CHECK-NEXT: } +// CHECK-NEXT: } +// CHECK-NEXT: poly.template @PartialRollbackCaller { +// CHECK-NEXT: poly.param @M : index +// CHECK-NEXT: function.def @apply(%[[VAL_17:[0-9a-zA-Z_\.]+]]: !array.type<8,@M x i1>) -> i1 { +// CHECK-NEXT: %[[VAL_18:[0-9a-zA-Z_\.]+]] = function.call @PartialRollbackSource::@apply<[8, @M, i1, 2]>(%[[VAL_17]]) : (!array.type<8,@M x i1>) -> i1 +// CHECK-NEXT: function.return %[[VAL_18]] : i1 +// CHECK-NEXT: } +// CHECK-NEXT: } +// CHECK-NEXT: poly.template @"PartialRollbackSource_8_\1A_b_2" { +// CHECK-NEXT: poly.param @M : index +// CHECK-NEXT: function.def @apply() -> index { +// CHECK-NEXT: %[[VAL_19:[0-9a-zA-Z_\.]+]] = arith.constant 2 : index +// CHECK-NEXT: function.return %[[VAL_19]] : index +// CHECK-NEXT: } +// CHECK-NEXT: } {poly.name_pattern = ["PartialRollbackSource_8_", "_b_2"]} +// CHECK-NEXT: poly.template @PartialRollbackSource_8 { +// CHECK-NEXT: poly.param @M : index +// CHECK-NEXT: poly.param @T : !poly.tvar<@T> +// CHECK-NEXT: poly.param @B +// CHECK-NEXT: function.def @apply() -> index { +// CHECK-NEXT: %[[VAL_20:[0-9a-zA-Z_\.]+]] = poly.read_const @B : index +// CHECK-NEXT: function.return %[[VAL_20]] : index +// CHECK-NEXT: } +// CHECK-NEXT: } +// CHECK-NEXT: poly.template @PartialRollbackCollisionCaller { +// CHECK-NEXT: poly.param @M : index +// CHECK-NEXT: function.def @apply() -> index { +// CHECK-NEXT: %[[VAL_21:[0-9a-zA-Z_\.]+]] = function.call @"PartialRollbackSource_8_\1A_b_2"::@apply<[@M]>() : () -> index +// CHECK-NEXT: function.return %[[VAL_21]] : index +// CHECK-NEXT: } +// CHECK-NEXT: } +// CHECK-NEXT: poly.template @"GoodRollbackSource_8_\1A_f_1" { +// CHECK-NEXT: poly.param @M : index +// CHECK-NEXT: function.def @apply(%[[VAL_22:[0-9a-zA-Z_\.]+]]: !array.type<8,@M x !felt.type<"bn128">>) -> !felt.type<"bn128"> { +// CHECK-NEXT: %[[VAL_23:[0-9a-zA-Z_\.]+]] = arith.constant true +// CHECK-NEXT: %[[VAL_24:[0-9a-zA-Z_\.]+]] = arith.constant 0 : index +// CHECK-NEXT: %[[VAL_25:[0-9a-zA-Z_\.]+]] = array.read %[[VAL_22]]{{\[}}%[[VAL_24]], %[[VAL_24]]] : <8,@M x !felt.type<"bn128">>, !felt.type<"bn128"> +// CHECK-NEXT: function.return %[[VAL_25]] : !felt.type<"bn128"> +// CHECK-NEXT: } +// CHECK-NEXT: } {poly.name_pattern = ["GoodRollbackSource_8_", "_f_1"]} +// CHECK-NEXT: poly.template @GoodRollbackSource { +// CHECK-NEXT: poly.param @N : index +// CHECK-NEXT: poly.param @M : index +// CHECK-NEXT: poly.param @T : !poly.tvar<@T> +// CHECK-NEXT: poly.param @B +// CHECK-NEXT: function.def @apply(%[[VAL_26:[0-9a-zA-Z_\.]+]]: !array.type<@N,@M x !poly.tvar<@T>>) -> !poly.tvar<@T> { +// CHECK-NEXT: %[[VAL_27:[0-9a-zA-Z_\.]+]] = poly.read_const @B : i1 +// CHECK-NEXT: %[[VAL_28:[0-9a-zA-Z_\.]+]] = arith.constant 0 : index +// CHECK-NEXT: %[[VAL_29:[0-9a-zA-Z_\.]+]] = array.read %[[VAL_26]]{{\[}}%[[VAL_28]], %[[VAL_28]]] : <@N,@M x !poly.tvar<@T>>, !poly.tvar<@T> +// CHECK-NEXT: function.return %[[VAL_29]] : !poly.tvar<@T> +// CHECK-NEXT: } +// CHECK-NEXT: } +// CHECK-NEXT: function.def @GoodRollbackCaller_9_apply(%[[VAL_30:[0-9a-zA-Z_\.]+]]: !array.type<8,9 x !felt.type<"bn128">>) -> !felt.type<"bn128"> { +// CHECK-NEXT: %[[VAL_31:[0-9a-zA-Z_\.]+]] = function.call @"GoodRollbackSource_8_\1A_f_1"::@apply<[9]>(%[[VAL_30]]) : (!array.type<8,9 x !felt.type<"bn128">>) -> !felt.type<"bn128"> +// CHECK-NEXT: function.return %[[VAL_31]] : !felt.type<"bn128"> +// CHECK-NEXT: } +// CHECK-NEXT: poly.template @GoodRollbackCaller { +// CHECK-NEXT: poly.param @M : index +// CHECK-NEXT: function.def @apply(%[[VAL_32:[0-9a-zA-Z_\.]+]]: !array.type<8,@M x !felt.type<"bn128">>) -> !felt.type<"bn128"> { +// CHECK-NEXT: %[[VAL_33:[0-9a-zA-Z_\.]+]] = function.call @"GoodRollbackSource_8_\1A_f_1"::@apply<[@M]>(%[[VAL_32]]) : (!array.type<8,@M x !felt.type<"bn128">>) -> !felt.type<"bn128"> +// CHECK-NEXT: function.return %[[VAL_33]] : !felt.type<"bn128"> +// CHECK-NEXT: } +// CHECK-NEXT: } +// CHECK-NEXT: function.def @call_good_after_rollback(%[[VAL_34:[0-9a-zA-Z_\.]+]]: !array.type<8,9 x !felt.type<"bn128">>) -> !felt.type<"bn128"> { +// CHECK-NEXT: %[[VAL_35:[0-9a-zA-Z_\.]+]] = function.call @GoodRollbackCaller_9_apply(%[[VAL_34]]) : (!array.type<8,9 x !felt.type<"bn128">>) -> !felt.type<"bn128"> +// CHECK-NEXT: function.return %[[VAL_35]] : !felt.type<"bn128"> +// CHECK-NEXT: } +// CHECK-NEXT: } diff --git a/test/Transforms/Flattening/instantiate_funcs_fail.llzk b/test/Transforms/Flattening/instantiate_funcs_fail.llzk index b4e5b97c21..d7f0943eb4 100644 --- a/test/Transforms/Flattening/instantiate_funcs_fail.llzk +++ b/test/Transforms/Flattening/instantiate_funcs_fail.llzk @@ -1,6 +1,7 @@ // RUN: llzk-opt -split-input-file -llzk-flatten -verify-diagnostics %s 2>&1 | FileCheck --enable-var-scope %s -// Tests: attempt to instantiate with incompatible types via `poly.tvar` (no template params on CallOp) +// Signature inference must reject a transitive `poly.tvar` specialization whose nested call has +// incompatible operand and result types. module attributes {llzk.lang} { poly.template @template_a1 { poly.param @N : index @@ -38,7 +39,7 @@ module attributes {llzk.lang} { // CHECK: llzk-flatten failed while instantiating functions in templates // ----- -// Tests: attempt to instantiate with incompatible types via `poly.tvar` (with template params on CallOp) +// The same transitive mismatch must roll back when the nested call has an explicit argument. module attributes {llzk.lang} { poly.template @template_b1 { poly.param @N : index @@ -53,7 +54,11 @@ module attributes {llzk.lang} { poly.param @N : index poly.param @M : index poly.param @T : !poly.tvar<@T> + poly.param @B function.def @f(%inp: !array.type<@N,@M x !poly.tvar<@T>>) -> !poly.tvar<@T> { + // Converting this read queues a warning, which must not escape when nested-call verification + // rejects and rolls back the prospective specialization below. + %flag = poly.read_const @B : i1 // expected-error@+2 {{'function.call' op operand type mismatch: expected type '!array.type<8,9 x !felt.type>', but found '!array.type<8,9 x i1>' for operand number 0}} // expected-error@+1 {{'function.call' op result type mismatch: expected type '!felt.type', but found 'i1' for result number 0}} %2 = function.call @template_b1::@f<[9]>(%inp) : (!array.type<@N,@M x !poly.tvar<@T>>) -> !poly.tvar<@T> @@ -64,8 +69,8 @@ module attributes {llzk.lang} { struct.def @Main { function.def @compute(%z: !array.type<8,9 x i1>) -> !struct.type<@Main> { %self = struct.new : !struct.type<@Main> - // expected-error@+1 {{failure while creating instantiated function 'template_b2_8_9_b_f'}} - function.call @template_b2::@f<[8,9,i1]>(%z) : (!array.type<8,9 x i1>) -> i1 + // expected-error@+1 {{failure while creating instantiated function}} + function.call @template_b2::@f<[8,9,i1,2]>(%z) : (!array.type<8,9 x i1>) -> i1 function.return %self : !struct.type<@Main> } function.def @constrain(%self: !struct.type<@Main>, %z: !array.type<8,9 x i1>) { diff --git a/test/Transforms/Flattening/instantiate_funcs_pass.llzk b/test/Transforms/Flattening/instantiate_funcs_pass.llzk index 89468d7776..6de91f9435 100644 --- a/test/Transforms/Flattening/instantiate_funcs_pass.llzk +++ b/test/Transforms/Flattening/instantiate_funcs_pass.llzk @@ -1,6 +1,7 @@ // RUN: llzk-opt -split-input-file -llzk-flatten -verify-diagnostics %s | FileCheck --enable-var-scope %s -// Test instantiation of templates with function member. +// Signature-inferred index bindings create concrete free-function clones and materialize their +// parameter reads. module attributes {llzk.lang} { poly.template @template_f1 { poly.param @N : index @@ -37,37 +38,38 @@ module attributes {llzk.lang} { } } } -// CHECK-LABEL: function.def @template_f1_9_f( -// CHECK-SAME: %[[VAL_0:[0-9a-zA-Z_\.]+]]: !array.type<8,5,9 x !felt.type>) -> !array.type<9 x !felt.type> { -// CHECK-NEXT: %[[VAL_1:[0-9a-zA-Z_\.]+]] = arith.constant 0 : index -// CHECK-NEXT: %[[VAL_2:[0-9a-zA-Z_\.]+]] = array.extract %[[VAL_0]]{{\[}}%[[VAL_1]], %[[VAL_1]]] : <8,5,9 x !felt.type> -// CHECK-NEXT: function.return %[[VAL_2]] : !array.type<9 x !felt.type> -// CHECK-NEXT: } -// -// CHECK-LABEL: function.def @template_f2_12_f( -// CHECK-SAME: %[[VAL_3:[0-9a-zA-Z_\.]+]]: !array.type<8,12 x !felt.type>) -> !felt.type { -// CHECK-NEXT: %[[VAL_4:[0-9a-zA-Z_\.]+]] = arith.constant 12 : index -// CHECK-NEXT: %[[VAL_5:[0-9a-zA-Z_\.]+]] = arith.constant 0 : index -// CHECK-NEXT: %[[VAL_6:[0-9a-zA-Z_\.]+]] = array.read %[[VAL_3]]{{\[}}%[[VAL_5]], %[[VAL_5]]] : <8,12 x !felt.type>, !felt.type -// CHECK-NEXT: %[[VAL_7:[0-9a-zA-Z_\.]+]] = cast.tofelt %[[VAL_4]] : index, !felt.type -// CHECK-NEXT: %[[VAL_8:[0-9a-zA-Z_\.]+]] = felt.add %[[VAL_6]], %[[VAL_7]] : !felt.type, !felt.type -// CHECK-NEXT: function.return %[[VAL_8]] : !felt.type -// CHECK-NEXT: } -// -// CHECK-LABEL: struct.def @Main { -// CHECK-NEXT: function.def @compute(%[[VAL_12:[0-9a-zA-Z_\.]+]]: !array.type<8,5,9 x !felt.type>, %[[VAL_13:[0-9a-zA-Z_\.]+]]: !array.type<8,12 x !felt.type>) -> !struct.type<@Main> attributes {function.allow_witness} { -// CHECK-NEXT: %[[VAL_14:[0-9a-zA-Z_\.]+]] = struct.new : <@Main> -// CHECK-NEXT: %[[VAL_15:[0-9a-zA-Z_\.]+]] = function.call @template_f1_9_f(%[[VAL_12]]) : (!array.type<8,5,9 x !felt.type>) -> !array.type<9 x !felt.type> -// CHECK-NEXT: %[[VAL_16:[0-9a-zA-Z_\.]+]] = function.call @template_f2_12_f(%[[VAL_13]]) : (!array.type<8,12 x !felt.type>) -> !felt.type -// CHECK-NEXT: function.return %[[VAL_14]] : !struct.type<@Main> + +// CHECK-LABEL: module attributes {llzk.lang} { +// CHECK-NEXT: function.def @template_f1_9_f(%[[VAL_0:[0-9a-zA-Z_\.]+]]: !array.type<8,5,9 x !felt.type>) -> !array.type<9 x !felt.type> { +// CHECK-NEXT: %[[VAL_1:[0-9a-zA-Z_\.]+]] = arith.constant 0 : index +// CHECK-NEXT: %[[VAL_2:[0-9a-zA-Z_\.]+]] = array.extract %[[VAL_0]]{{\[}}%[[VAL_1]], %[[VAL_1]]] : <8,5,9 x !felt.type> +// CHECK-NEXT: function.return %[[VAL_2]] : !array.type<9 x !felt.type> // CHECK-NEXT: } -// CHECK-NEXT: function.def @constrain(%[[VAL_17:[0-9a-zA-Z_\.]+]]: !struct.type<@Main>, %[[VAL_18:[0-9a-zA-Z_\.]+]]: !array.type<8,5,9 x !felt.type>, %[[VAL_19:[0-9a-zA-Z_\.]+]]: !array.type<8,12 x !felt.type>) attributes {function.allow_constraint} { -// CHECK-NEXT: function.return +// CHECK-NEXT: function.def @template_f2_12_f(%[[VAL_3:[0-9a-zA-Z_\.]+]]: !array.type<8,12 x !felt.type>) -> !felt.type { +// CHECK-NEXT: %[[VAL_4:[0-9a-zA-Z_\.]+]] = arith.constant 12 : index +// CHECK-NEXT: %[[VAL_5:[0-9a-zA-Z_\.]+]] = arith.constant 0 : index +// CHECK-NEXT: %[[VAL_6:[0-9a-zA-Z_\.]+]] = array.read %[[VAL_3]]{{\[}}%[[VAL_5]], %[[VAL_5]]] : <8,12 x !felt.type>, !felt.type +// CHECK-NEXT: %[[VAL_7:[0-9a-zA-Z_\.]+]] = cast.tofelt %[[VAL_4]] : index, !felt.type +// CHECK-NEXT: %[[VAL_8:[0-9a-zA-Z_\.]+]] = felt.add %[[VAL_6]], %[[VAL_7]] : !felt.type, !felt.type +// CHECK-NEXT: function.return %[[VAL_8]] : !felt.type +// CHECK-NEXT: } +// CHECK-NEXT: struct.def @Main { +// CHECK-NEXT: function.def @compute(%[[VAL_9:[0-9a-zA-Z_\.]+]]: !array.type<8,5,9 x !felt.type>, %[[VAL_10:[0-9a-zA-Z_\.]+]]: !array.type<8,12 x !felt.type>) -> !struct.type<@Main> attributes {function.allow_witness} { +// CHECK-NEXT: %[[VAL_11:[0-9a-zA-Z_\.]+]] = struct.new : <@Main> +// CHECK-NEXT: %[[VAL_12:[0-9a-zA-Z_\.]+]] = function.call @template_f1_9_f(%[[VAL_9]]) : (!array.type<8,5,9 x !felt.type>) -> !array.type<9 x !felt.type> +// CHECK-NEXT: %[[VAL_13:[0-9a-zA-Z_\.]+]] = function.call @template_f2_12_f(%[[VAL_10]]) : (!array.type<8,12 x !felt.type>) -> !felt.type +// CHECK-NEXT: function.return %[[VAL_11]] : !struct.type<@Main> +// CHECK-NEXT: } +// CHECK-NEXT: function.def @constrain(%[[VAL_14:[0-9a-zA-Z_\.]+]]: !struct.type<@Main>, %[[VAL_15:[0-9a-zA-Z_\.]+]]: !array.type<8,5,9 x !felt.type>, %[[VAL_16:[0-9a-zA-Z_\.]+]]: !array.type<8,12 x !felt.type>) attributes {function.allow_constraint} { +// CHECK-NEXT: function.return +// CHECK-NEXT: } // CHECK-NEXT: } // CHECK-NEXT: } + // ----- -// Test transitive instantiation of functions. Also different instantiations of the same function. +// A concrete outer call specializes its nested target, while a direct call with another binding +// retains a distinct specialization of that target. module attributes {llzk.lang} { poly.template @template_g1 { poly.param @N : index @@ -85,7 +87,7 @@ module attributes {llzk.lang} { poly.param @T : !poly.tvar<@T> function.def @f(%inp: !array.type<@N,@M x !poly.tvar<@T>>) -> !poly.tvar<@T> { - // Not a common use case to call a more-specific template but it is possible. + // The outer signature has more dimensions than the nested target, but both still unify. %2 = function.call @template_g1::@f(%inp) : (!array.type<@N,@M x !poly.tvar<@T>>) -> !poly.tvar<@T> function.return %2 : !poly.tvar<@T> } @@ -103,39 +105,39 @@ module attributes {llzk.lang} { } } } -// CHECK-LABEL: function.def @template_g1_12_f( -// CHECK-SAME: %[[VAL_0:[0-9a-zA-Z_\.]+]]: !array.type<8,12 x !felt.type>) -> !felt.type { -// CHECK-NEXT: %[[VAL_1:[0-9a-zA-Z_\.]+]] = arith.constant 0 : index -// CHECK-NEXT: %[[VAL_2:[0-9a-zA-Z_\.]+]] = array.read %[[VAL_0]]{{\[}}%[[VAL_1]], %[[VAL_1]]] : <8,12 x !felt.type>, !felt.type -// CHECK-NEXT: function.return %[[VAL_2]] : !felt.type -// CHECK-NEXT: } -// -// CHECK-LABEL: function.def @template_g1_9_f( -// CHECK-SAME: %[[VAL_3:[0-9a-zA-Z_\.]+]]: !array.type<8,9 x !felt.type>) -> !felt.type { -// CHECK-NEXT: %[[VAL_4:[0-9a-zA-Z_\.]+]] = arith.constant 0 : index -// CHECK-NEXT: %[[VAL_5:[0-9a-zA-Z_\.]+]] = array.read %[[VAL_3]]{{\[}}%[[VAL_4]], %[[VAL_4]]] : <8,9 x !felt.type>, !felt.type -// CHECK-NEXT: function.return %[[VAL_5]] : !felt.type -// CHECK-NEXT: } -// -// CHECK-LABEL: function.def @template_g2_8_9_f_f( -// CHECK-SAME: %[[VAL_9:[0-9a-zA-Z_\.]+]]: !array.type<8,9 x !felt.type>) -> !felt.type { -// CHECK-NEXT: %[[VAL_10:[0-9a-zA-Z_\.]+]] = function.call @template_g1_9_f(%[[VAL_9]]) : (!array.type<8,9 x !felt.type>) -> !felt.type -// CHECK-NEXT: function.return %[[VAL_10]] : !felt.type -// CHECK-NEXT: } -// -// CHECK-LABEL: struct.def @Main { -// CHECK-NEXT: function.def @compute(%[[VAL_13:[0-9a-zA-Z_\.]+]]: !array.type<8,12 x !felt.type>, %[[VAL_14:[0-9a-zA-Z_\.]+]]: !array.type<8,9 x !felt.type>) -> !struct.type<@Main> attributes {function.allow_witness} { -// CHECK-NEXT: %[[VAL_15:[0-9a-zA-Z_\.]+]] = struct.new : <@Main> -// CHECK-NEXT: %[[VAL_16:[0-9a-zA-Z_\.]+]] = function.call @template_g1_12_f(%[[VAL_13]]) : (!array.type<8,12 x !felt.type>) -> !felt.type -// CHECK-NEXT: %[[VAL_17:[0-9a-zA-Z_\.]+]] = function.call @template_g2_8_9_f_f(%[[VAL_14]]) : (!array.type<8,9 x !felt.type>) -> !felt.type -// CHECK-NEXT: function.return %[[VAL_15]] : !struct.type<@Main> + +// CHECK-LABEL: module attributes {llzk.lang} { +// CHECK-NEXT: function.def @template_g1_12_f(%[[VAL_0:[0-9a-zA-Z_\.]+]]: !array.type<8,12 x !felt.type>) -> !felt.type { +// CHECK-NEXT: %[[VAL_1:[0-9a-zA-Z_\.]+]] = arith.constant 0 : index +// CHECK-NEXT: %[[VAL_2:[0-9a-zA-Z_\.]+]] = array.read %[[VAL_0]]{{\[}}%[[VAL_1]], %[[VAL_1]]] : <8,12 x !felt.type>, !felt.type +// CHECK-NEXT: function.return %[[VAL_2]] : !felt.type // CHECK-NEXT: } -// CHECK-NEXT: function.def @constrain(%[[VAL_18:[0-9a-zA-Z_\.]+]]: !struct.type<@Main>, %[[VAL_19:[0-9a-zA-Z_\.]+]]: !array.type<8,12 x !felt.type>, %[[VAL_20:[0-9a-zA-Z_\.]+]]: !array.type<8,9 x !felt.type>) attributes {function.allow_constraint} { -// CHECK-NEXT: function.return +// CHECK-NEXT: function.def @template_g1_9_f(%[[VAL_3:[0-9a-zA-Z_\.]+]]: !array.type<8,9 x !felt.type>) -> !felt.type { +// CHECK-NEXT: %[[VAL_4:[0-9a-zA-Z_\.]+]] = arith.constant 0 : index +// CHECK-NEXT: %[[VAL_5:[0-9a-zA-Z_\.]+]] = array.read %[[VAL_3]]{{\[}}%[[VAL_4]], %[[VAL_4]]] : <8,9 x !felt.type>, !felt.type +// CHECK-NEXT: function.return %[[VAL_5]] : !felt.type +// CHECK-NEXT: } +// CHECK-NEXT: function.def @template_g2_8_9_f_f(%[[VAL_6:[0-9a-zA-Z_\.]+]]: !array.type<8,9 x !felt.type>) -> !felt.type { +// CHECK-NEXT: %[[VAL_7:[0-9a-zA-Z_\.]+]] = function.call @template_g1_9_f(%[[VAL_6]]) : (!array.type<8,9 x !felt.type>) -> !felt.type +// CHECK-NEXT: function.return %[[VAL_7]] : !felt.type +// CHECK-NEXT: } +// CHECK-NEXT: struct.def @Main { +// CHECK-NEXT: function.def @compute(%[[VAL_8:[0-9a-zA-Z_\.]+]]: !array.type<8,12 x !felt.type>, %[[VAL_9:[0-9a-zA-Z_\.]+]]: !array.type<8,9 x !felt.type>) -> !struct.type<@Main> attributes {function.allow_witness} { +// CHECK-NEXT: %[[VAL_10:[0-9a-zA-Z_\.]+]] = struct.new : <@Main> +// CHECK-NEXT: %[[VAL_11:[0-9a-zA-Z_\.]+]] = function.call @template_g1_12_f(%[[VAL_8]]) : (!array.type<8,12 x !felt.type>) -> !felt.type +// CHECK-NEXT: %[[VAL_12:[0-9a-zA-Z_\.]+]] = function.call @template_g2_8_9_f_f(%[[VAL_9]]) : (!array.type<8,9 x !felt.type>) -> !felt.type +// CHECK-NEXT: function.return %[[VAL_10]] : !struct.type<@Main> +// CHECK-NEXT: } +// CHECK-NEXT: function.def @constrain(%[[VAL_13:[0-9a-zA-Z_\.]+]]: !struct.type<@Main>, %[[VAL_14:[0-9a-zA-Z_\.]+]]: !array.type<8,12 x !felt.type>, %[[VAL_15:[0-9a-zA-Z_\.]+]]: !array.type<8,9 x !felt.type>) attributes {function.allow_constraint} { +// CHECK-NEXT: function.return +// CHECK-NEXT: } // CHECK-NEXT: } // CHECK-NEXT: } + // ----- +// A nested call with one concrete and one symbolic dimension creates a reduced callee template and +// retargets the outer body to that partial specialization. module attributes {llzk.lang} { poly.template @template_h1 { poly.param @A : index @@ -157,26 +159,29 @@ module attributes {llzk.lang} { } } } -// CHECK-LABEL: poly.template @"template_h1_8_\1A" { -// CHECK-NEXT: poly.param @B : index -// CHECK-NEXT: function.def @f(%[[VAL_0:[0-9a-zA-Z_\.]+]]: !array.type<8,@B x !felt.type>) -> !felt.type { -// CHECK-NEXT: %[[VAL_1:[0-9a-zA-Z_\.]+]] = arith.constant 0 : index -// CHECK-NEXT: %[[VAL_2:[0-9a-zA-Z_\.]+]] = array.read %[[VAL_0]]{{\[}}%[[VAL_1]], %[[VAL_1]]] : <8,@B x !felt.type>, !felt.type -// CHECK-NEXT: function.return %[[VAL_2]] : !felt.type -// CHECK-NEXT: } -// CHECK-NEXT: } -// -// CHECK-LABEL: poly.template @template_h2 { -// CHECK-NEXT: poly.param @N : index -// CHECK-NEXT: function.def @g(%[[VAL_6:[0-9a-zA-Z_\.]+]]: !array.type<8,@N x !felt.type>) -> !felt.type { -// CHECK-NEXT: %[[VAL_7:[0-9a-zA-Z_\.]+]] = function.call @"template_h1_8_\1A"::@f(%[[VAL_6]]) : (!array.type<8,@N x !felt.type>) -> !felt.type -// CHECK-NEXT: function.return %[[VAL_7]] : !felt.type + +// CHECK-LABEL: module attributes {llzk.lang} { +// CHECK-NEXT: poly.template @"template_h1_8_\1A" { +// CHECK-NEXT: poly.param @B : index +// CHECK-NEXT: function.def @f(%[[VAL_0:[0-9a-zA-Z_\.]+]]: !array.type<8,@B x !felt.type>) -> !felt.type { +// CHECK-NEXT: %[[VAL_1:[0-9a-zA-Z_\.]+]] = arith.constant 0 : index +// CHECK-NEXT: %[[VAL_2:[0-9a-zA-Z_\.]+]] = array.read %[[VAL_0]]{{\[}}%[[VAL_1]], %[[VAL_1]]] : <8,@B x !felt.type>, !felt.type +// CHECK-NEXT: function.return %[[VAL_2]] : !felt.type +// CHECK-NEXT: } +// CHECK-NEXT: } {poly.name_pattern = ["template_h1_8_", ""]} +// CHECK-NEXT: poly.template @template_h2 { +// CHECK-NEXT: poly.param @N : index +// CHECK-NEXT: function.def @g(%[[VAL_3:[0-9a-zA-Z_\.]+]]: !array.type<8,@N x !felt.type>) -> !felt.type { +// CHECK-NEXT: %[[VAL_4:[0-9a-zA-Z_\.]+]] = function.call @"template_h1_8_\1A"::@f(%[[VAL_3]]) : (!array.type<8,@N x !felt.type>) -> !felt.type +// CHECK-NEXT: function.return %[[VAL_4]] : !felt.type +// CHECK-NEXT: } // CHECK-NEXT: } // CHECK-NEXT: } + // ----- -// Similar to previous but there is eventually a concrete instantiation of intermediate partial instantiations. -// No cleanup runs in this test so the partials are left in the output IR. +// A later call supplies the final dimension, so both the outer and nested functions become fully +// specialized; preimage cleanup then removes their parameterized sources. module attributes {llzk.lang} { poly.template @template_k1 { poly.param @A : index @@ -209,32 +214,33 @@ module attributes {llzk.lang} { } } } -// CHECK-LABEL: function.def @template_k1_8_5_f( -// CHECK-SAME: %[[VAL_0:[0-9a-zA-Z_\.]+]]: !array.type<8,5 x !felt.type>) -> !felt.type { -// CHECK-NEXT: %[[VAL_1:[0-9a-zA-Z_\.]+]] = arith.constant 0 : index -// CHECK-NEXT: %[[VAL_2:[0-9a-zA-Z_\.]+]] = array.read %[[VAL_0]]{{\[}}%[[VAL_1]], %[[VAL_1]]] : <8,5 x !felt.type>, !felt.type -// CHECK-NEXT: function.return %[[VAL_2]] : !felt.type -// CHECK-NEXT: } -// -// CHECK-LABEL: function.def @template_k2_5_g( -// CHECK-SAME: %[[VAL_9:[0-9a-zA-Z_\.]+]]: !array.type<8,5 x !felt.type>) -> !felt.type { -// CHECK-NEXT: %[[VAL_10:[0-9a-zA-Z_\.]+]] = function.call @template_k1_8_5_f(%[[VAL_9]]) : (!array.type<8,5 x !felt.type>) -> !felt.type -// CHECK-NEXT: function.return %[[VAL_10]] : !felt.type -// CHECK-NEXT: } -// -// CHECK-LABEL: struct.def @Main { -// CHECK-NEXT: function.def @compute(%[[VAL_13:[0-9a-zA-Z_\.]+]]: !array.type<8,5 x !felt.type>) -> !struct.type<@Main> attributes {function.allow_witness} { -// CHECK-NEXT: %[[VAL_14:[0-9a-zA-Z_\.]+]] = struct.new : <@Main> -// CHECK-NEXT: %[[VAL_15:[0-9a-zA-Z_\.]+]] = function.call @template_k2_5_g(%[[VAL_13]]) : (!array.type<8,5 x !felt.type>) -> !felt.type -// CHECK-NEXT: function.return %[[VAL_14]] : !struct.type<@Main> + +// CHECK-LABEL: module attributes {llzk.lang} { +// CHECK-NEXT: function.def @template_k1_8_5_f(%[[VAL_0:[0-9a-zA-Z_\.]+]]: !array.type<8,5 x !felt.type>) -> !felt.type { +// CHECK-NEXT: %[[VAL_1:[0-9a-zA-Z_\.]+]] = arith.constant 0 : index +// CHECK-NEXT: %[[VAL_2:[0-9a-zA-Z_\.]+]] = array.read %[[VAL_0]]{{\[}}%[[VAL_1]], %[[VAL_1]]] : <8,5 x !felt.type>, !felt.type +// CHECK-NEXT: function.return %[[VAL_2]] : !felt.type +// CHECK-NEXT: } +// CHECK-NEXT: function.def @template_k2_5_g(%[[VAL_3:[0-9a-zA-Z_\.]+]]: !array.type<8,5 x !felt.type>) -> !felt.type { +// CHECK-NEXT: %[[VAL_4:[0-9a-zA-Z_\.]+]] = function.call @template_k1_8_5_f(%[[VAL_3]]) : (!array.type<8,5 x !felt.type>) -> !felt.type +// CHECK-NEXT: function.return %[[VAL_4]] : !felt.type // CHECK-NEXT: } -// CHECK-NEXT: function.def @constrain(%[[VAL_16:[0-9a-zA-Z_\.]+]]: !struct.type<@Main>, %[[VAL_17:[0-9a-zA-Z_\.]+]]: !array.type<8,5 x !felt.type>) attributes {function.allow_constraint} { -// CHECK-NEXT: function.return +// CHECK-NEXT: struct.def @Main { +// CHECK-NEXT: function.def @compute(%[[VAL_5:[0-9a-zA-Z_\.]+]]: !array.type<8,5 x !felt.type>) -> !struct.type<@Main> attributes {function.allow_witness} { +// CHECK-NEXT: %[[VAL_6:[0-9a-zA-Z_\.]+]] = struct.new : <@Main> +// CHECK-NEXT: %[[VAL_7:[0-9a-zA-Z_\.]+]] = function.call @template_k2_5_g(%[[VAL_5]]) : (!array.type<8,5 x !felt.type>) -> !felt.type +// CHECK-NEXT: function.return %[[VAL_6]] : !struct.type<@Main> +// CHECK-NEXT: } +// CHECK-NEXT: function.def @constrain(%[[VAL_8:[0-9a-zA-Z_\.]+]]: !struct.type<@Main>, %[[VAL_9:[0-9a-zA-Z_\.]+]]: !array.type<8,5 x !felt.type>) attributes {function.allow_constraint} { +// CHECK-NEXT: function.return +// CHECK-NEXT: } // CHECK-NEXT: } // CHECK-NEXT: } + // ----- -// Test instantiation of function with template parameters specified at call site. +// An explicit argument specializes a parameter absent from the function signature and materializes +// its `poly.read_const`. module attributes {llzk.lang} { poly.template @GlobalFunc { poly.param @A : index @@ -255,6 +261,7 @@ module attributes {llzk.lang} { function.def @constrain(%self: !struct.type<@CallKnownTemplateGlobal>) { function.return } } } + // CHECK-LABEL: module attributes {llzk.lang} { // CHECK-NEXT: function.def @GlobalFunc_89_id() -> index { // CHECK-NEXT: %[[VAL_0:[0-9a-zA-Z_\.]+]] = arith.constant 89 : index @@ -271,9 +278,11 @@ module attributes {llzk.lang} { // CHECK-NEXT: } // CHECK-NEXT: } // CHECK-NEXT: } + // ----- -// Test instantiation of function with template parameters specified at call site. +// A nested explicit argument remains attached while the outer function is cloned, then materializes +// before the nested target is specialized. module attributes {llzk.lang} { poly.template @template_b1 { poly.param @X : index @@ -289,7 +298,7 @@ module attributes {llzk.lang} { poly.param @M : index poly.param @T : !poly.tvar<@T> function.def @f(%inp: !array.type<@N,@M x !poly.tvar<@T>>) -> !poly.tvar<@T> { - %2 = function.call @template_b1::@f<[@N]>(%inp) : (!array.type<@N,@M x !poly.tvar<@T>>) -> !poly.tvar<@T> + %2 = function.call @template_b1::@f<[@M]>(%inp) : (!array.type<@N,@M x !poly.tvar<@T>>) -> !poly.tvar<@T> function.return %2 : !poly.tvar<@T> } } @@ -305,32 +314,32 @@ module attributes {llzk.lang} { } } } -// CHECK-LABEL: function.def @template_b1_9_f( -// CHECK-SAME: %[[VAL_0:[0-9a-zA-Z_\.]+]]: !array.type<8,9 x !felt.type>) -> !felt.type { -// CHECK-NEXT: %[[VAL_1:[0-9a-zA-Z_\.]+]] = arith.constant 0 : index -// CHECK-NEXT: %[[VAL_2:[0-9a-zA-Z_\.]+]] = array.read %[[VAL_0]]{{\[}}%[[VAL_1]], %[[VAL_1]]] : <8,9 x !felt.type>, !felt.type -// CHECK-NEXT: function.return %[[VAL_2]] : !felt.type -// CHECK-NEXT: } -// -// CHECK-LABEL: function.def @template_b2_8_9_f_f( -// CHECK-SAME: %[[VAL_6:[0-9a-zA-Z_\.]+]]: !array.type<8,9 x !felt.type>) -> !felt.type { -// CHECK-NEXT: %[[VAL_7:[0-9a-zA-Z_\.]+]] = function.call @template_b1_9_f(%[[VAL_6]]) : (!array.type<8,9 x !felt.type>) -> !felt.type -// CHECK-NEXT: function.return %[[VAL_7]] : !felt.type -// CHECK-NEXT: } -// -// CHECK-LABEL: struct.def @Main { -// CHECK-NEXT: function.def @compute(%[[VAL_10:[0-9a-zA-Z_\.]+]]: !array.type<8,9 x !felt.type>) -> !struct.type<@Main> attributes {function.allow_witness} { -// CHECK-NEXT: %[[VAL_11:[0-9a-zA-Z_\.]+]] = struct.new : <@Main> -// CHECK-NEXT: %[[VAL_12:[0-9a-zA-Z_\.]+]] = function.call @template_b2_8_9_f_f(%[[VAL_10]]) : (!array.type<8,9 x !felt.type>) -> !felt.type -// CHECK-NEXT: function.return %[[VAL_11]] : !struct.type<@Main> + +// CHECK-LABEL: module attributes {llzk.lang} { +// CHECK-NEXT: function.def @template_b1_9_f(%[[VAL_0:[0-9a-zA-Z_\.]+]]: !array.type<8,9 x !felt.type>) -> !felt.type { +// CHECK-NEXT: %[[VAL_1:[0-9a-zA-Z_\.]+]] = arith.constant 0 : index +// CHECK-NEXT: %[[VAL_2:[0-9a-zA-Z_\.]+]] = array.read %[[VAL_0]]{{\[}}%[[VAL_1]], %[[VAL_1]]] : <8,9 x !felt.type>, !felt.type +// CHECK-NEXT: function.return %[[VAL_2]] : !felt.type // CHECK-NEXT: } -// CHECK-NEXT: function.def @constrain(%[[VAL_13:[0-9a-zA-Z_\.]+]]: !struct.type<@Main>, %[[VAL_14:[0-9a-zA-Z_\.]+]]: !array.type<8,9 x !felt.type>) attributes {function.allow_constraint} { -// CHECK-NEXT: function.return +// CHECK-NEXT: function.def @template_b2_8_9_f_f(%[[VAL_3:[0-9a-zA-Z_\.]+]]: !array.type<8,9 x !felt.type>) -> !felt.type { +// CHECK-NEXT: %[[VAL_4:[0-9a-zA-Z_\.]+]] = function.call @template_b1_9_f(%[[VAL_3]]) : (!array.type<8,9 x !felt.type>) -> !felt.type +// CHECK-NEXT: function.return %[[VAL_4]] : !felt.type +// CHECK-NEXT: } +// CHECK-NEXT: struct.def @Main { +// CHECK-NEXT: function.def @compute(%[[VAL_5:[0-9a-zA-Z_\.]+]]: !array.type<8,9 x !felt.type>) -> !struct.type<@Main> attributes {function.allow_witness} { +// CHECK-NEXT: %[[VAL_6:[0-9a-zA-Z_\.]+]] = struct.new : <@Main> +// CHECK-NEXT: %[[VAL_7:[0-9a-zA-Z_\.]+]] = function.call @template_b2_8_9_f_f(%[[VAL_5]]) : (!array.type<8,9 x !felt.type>) -> !felt.type +// CHECK-NEXT: function.return %[[VAL_6]] : !struct.type<@Main> +// CHECK-NEXT: } +// CHECK-NEXT: function.def @constrain(%[[VAL_8:[0-9a-zA-Z_\.]+]]: !struct.type<@Main>, %[[VAL_9:[0-9a-zA-Z_\.]+]]: !array.type<8,9 x !felt.type>) attributes {function.allow_constraint} { +// CHECK-NEXT: function.return +// CHECK-NEXT: } // CHECK-NEXT: } // CHECK-NEXT: } + // ----- -// COM: `read_const` with a variable type +// A value parameter read through a type-variable result materializes both the value and result type. module attributes {llzk.lang} { poly.template @T { poly.param @Num @@ -347,12 +356,14 @@ module attributes {llzk.lang} { function.return %0 : index } } -// CHECK-LABEL: function.def @T_89_i_compute() -> index { -// CHECK-NEXT: %[[VAL_15:[0-9a-zA-Z_\.]+]] = arith.constant 89 : index -// CHECK-NEXT: function.return %[[VAL_15]] : index -// CHECK-NEXT: } -// -// CHECK-LABEL: function.def @main() -> index { -// CHECK-NEXT: %[[VAL_16:[0-9a-zA-Z_\.]+]] = function.call @T_89_i_compute() : () -> index -// CHECK-NEXT: function.return %[[VAL_16]] : index + +// CHECK-LABEL: module attributes {llzk.lang} { +// CHECK-NEXT: function.def @T_89_i_compute() -> index { +// CHECK-NEXT: %[[VAL_0:[0-9a-zA-Z_\.]+]] = arith.constant 89 : index +// CHECK-NEXT: function.return %[[VAL_0]] : index +// CHECK-NEXT: } +// CHECK-NEXT: function.def @main() -> index { +// CHECK-NEXT: %[[VAL_1:[0-9a-zA-Z_\.]+]] = function.call @T_89_i_compute() : () -> index +// CHECK-NEXT: function.return %[[VAL_1]] : index +// CHECK-NEXT: } // CHECK-NEXT: } diff --git a/test/Transforms/Flattening/instantiate_function_order.llzk b/test/Transforms/Flattening/instantiate_function_order.llzk new file mode 100644 index 0000000000..7f85999166 --- /dev/null +++ b/test/Transforms/Flattening/instantiate_function_order.llzk @@ -0,0 +1,48 @@ +// RUN: llzk-opt -split-input-file -llzk-flatten=cleanup=disabled %s | FileCheck %s + +// Ordered bindings are part of specialization identity: swapping them changes the result. +module attributes {llzk.lang} { + poly.template @Ordered { + poly.param @A : index + poly.param @B : index + + function.def @compute() -> index { + %a = poly.read_const @A : index + function.return %a : index + } + } + + function.def @main() -> index { + %first = function.call @Ordered::@compute<[1, 2]>() : () -> index + %first_again = function.call @Ordered::@compute<[1, 2]>() : () -> index + %second = function.call @Ordered::@compute<[2, 1]>() : () -> index + %second_again = function.call @Ordered::@compute<[2, 1]>() : () -> index + function.return %second_again : index + } +} + +// CHECK-LABEL: module attributes {llzk.lang} { +// CHECK-NEXT: function.def @Ordered_1_2_compute() -> index { +// CHECK-NEXT: %[[VAL_0:[0-9a-zA-Z_\.]+]] = arith.constant 1 : index +// CHECK-NEXT: function.return %[[VAL_0]] : index +// CHECK-NEXT: } +// CHECK-NEXT: function.def @Ordered_2_1_compute() -> index { +// CHECK-NEXT: %[[VAL_1:[0-9a-zA-Z_\.]+]] = arith.constant 2 : index +// CHECK-NEXT: function.return %[[VAL_1]] : index +// CHECK-NEXT: } +// CHECK-NEXT: poly.template @Ordered { +// CHECK-NEXT: poly.param @A : index +// CHECK-NEXT: poly.param @B : index +// CHECK-NEXT: function.def @compute() -> index { +// CHECK-NEXT: %[[VAL_2:[0-9a-zA-Z_\.]+]] = poly.read_const @A : index +// CHECK-NEXT: function.return %[[VAL_2]] : index +// CHECK-NEXT: } +// CHECK-NEXT: } +// CHECK-NEXT: function.def @main() -> index { +// CHECK-NEXT: %[[VAL_3:[0-9a-zA-Z_\.]+]] = function.call @Ordered_1_2_compute() : () -> index +// CHECK-NEXT: %[[VAL_4:[0-9a-zA-Z_\.]+]] = function.call @Ordered_1_2_compute() : () -> index +// CHECK-NEXT: %[[VAL_5:[0-9a-zA-Z_\.]+]] = function.call @Ordered_2_1_compute() : () -> index +// CHECK-NEXT: %[[VAL_6:[0-9a-zA-Z_\.]+]] = function.call @Ordered_2_1_compute() : () -> index +// CHECK-NEXT: function.return %[[VAL_6]] : index +// CHECK-NEXT: } +// CHECK-NEXT: } diff --git a/test/Transforms/Flattening/instantiate_function_reuse.llzk b/test/Transforms/Flattening/instantiate_function_reuse.llzk new file mode 100644 index 0000000000..8c3badd2dd --- /dev/null +++ b/test/Transforms/Flattening/instantiate_function_reuse.llzk @@ -0,0 +1,358 @@ +// RUN: llzk-opt -split-input-file -llzk-flatten=cleanup=disabled %s | FileCheck %s + +module attributes {llzk.lang} { + // A user-defined function collides with the preferred full-specialization name. The clone must + // be uniquified, and repeated calls with the same source and binding must reuse that name. + poly.template @T { + poly.param @N : index + function.def @compute() -> index { + %n = poly.read_const @N : index + function.return %n : index + } + + function.def @other() -> index { + %value = arith.constant 8 : index + function.return %value : index + } + } + + function.def @T_35_compute() -> index { + %wrong = arith.constant 99 : index + function.return %wrong : index + } + + function.def @main() -> index { + %got = function.call @T::@compute<[35]>() : () -> index + %got_again = function.call @T::@compute<[35]>() : () -> index + %different_binding = function.call @T::@compute<[36]>() : () -> index + %different_binding_again = function.call @T::@compute<[36]>() : () -> index + %different_source = function.call @T::@other<[35]>() : () -> index + %different_source_again = function.call @T::@other<[35]>() : () -> index + function.return %different_source_again : index + } +} + +// CHECK-LABEL: module attributes {llzk.lang} { +// CHECK-NEXT: function.def @T_35_compute_0() -> index { +// CHECK-NEXT: %[[VAL_0:[0-9a-zA-Z_\.]+]] = arith.constant 35 : index +// CHECK-NEXT: function.return %[[VAL_0]] : index +// CHECK-NEXT: } +// CHECK-NEXT: function.def @T_36_compute() -> index { +// CHECK-NEXT: %[[VAL_1:[0-9a-zA-Z_\.]+]] = arith.constant 36 : index +// CHECK-NEXT: function.return %[[VAL_1]] : index +// CHECK-NEXT: } +// CHECK-NEXT: function.def @T_35_other() -> index { +// CHECK-NEXT: %[[VAL_2:[0-9a-zA-Z_\.]+]] = arith.constant 8 : index +// CHECK-NEXT: function.return %[[VAL_2]] : index +// CHECK-NEXT: } +// CHECK-NEXT: poly.template @T { +// CHECK-NEXT: poly.param @N : index +// CHECK-NEXT: function.def @compute() -> index { +// CHECK-NEXT: %[[VAL_3:[0-9a-zA-Z_\.]+]] = poly.read_const @N : index +// CHECK-NEXT: function.return %[[VAL_3]] : index +// CHECK-NEXT: } +// CHECK-NEXT: function.def @other() -> index { +// CHECK-NEXT: %[[VAL_4:[0-9a-zA-Z_\.]+]] = arith.constant 8 : index +// CHECK-NEXT: function.return %[[VAL_4]] : index +// CHECK-NEXT: } +// CHECK-NEXT: } +// CHECK-NEXT: function.def @T_35_compute() -> index { +// CHECK-NEXT: %[[VAL_5:[0-9a-zA-Z_\.]+]] = arith.constant 99 : index +// CHECK-NEXT: function.return %[[VAL_5]] : index +// CHECK-NEXT: } +// CHECK-NEXT: function.def @main() -> index { +// CHECK-NEXT: %[[VAL_6:[0-9a-zA-Z_\.]+]] = function.call @T_35_compute_0() : () -> index +// CHECK-NEXT: %[[VAL_7:[0-9a-zA-Z_\.]+]] = function.call @T_35_compute_0() : () -> index +// CHECK-NEXT: %[[VAL_8:[0-9a-zA-Z_\.]+]] = function.call @T_36_compute() : () -> index +// CHECK-NEXT: %[[VAL_9:[0-9a-zA-Z_\.]+]] = function.call @T_36_compute() : () -> index +// CHECK-NEXT: %[[VAL_10:[0-9a-zA-Z_\.]+]] = function.call @T_35_other() : () -> index +// CHECK-NEXT: %[[VAL_11:[0-9a-zA-Z_\.]+]] = function.call @T_35_other() : () -> index +// CHECK-NEXT: function.return %[[VAL_11]] : index +// CHECK-NEXT: } +// CHECK-NEXT: } + +// ----- + +// Deferred expression preservation, canonical felt materialization, and source-based reuse meet +// here. Equivalent felt spellings must converge on the same uniquified specialization without +// retargeting to the user function that occupies its preferred generated name. +module attributes {llzk.lang} { + poly.template @Seam { + poly.param @F : !felt.type<"bn128"> + poly.param @M : !felt.type<"bn128"> + poly.expr @Sum { + %f = poly.read_const @F : !felt.type<"bn128"> + %m = poly.read_const @M : !felt.type<"bn128"> + %sum = felt.add %f, %m : !felt.type<"bn128">, !felt.type<"bn128"> + poly.yield %sum : !felt.type<"bn128"> + } + function.def @value() -> !felt.type<"bn128"> { + %sum = poly.read_const @Sum : !felt.type<"bn128"> + function.return %sum : !felt.type<"bn128"> + } + } + + function.def @"Seam_f<35:5:bn128>_f<7:5:bn128>_value"() -> !felt.type<"bn128"> { + %wrong = felt.const 99 : !felt.type<"bn128"> + function.return %wrong : !felt.type<"bn128"> + } + + poly.template @Bridge { + poly.param @M : !felt.type<"bn128"> + function.def @value() -> !felt.type<"bn128"> { + %fieldless = function.call @Seam::@value<[#felt, @M]>() : + () -> !felt.type<"bn128"> + %integer = function.call @Seam::@value<[35, @M]>() : () -> !felt.type<"bn128"> + %fielded = function.call @Seam::@value<[ + #felt>, @M + ]>() : () -> !felt.type<"bn128"> + function.return %fielded : !felt.type<"bn128"> + } + } + + function.def @main() -> !felt.type<"bn128"> { + %fieldless = function.call @Bridge::@value<[#felt]>() : + () -> !felt.type<"bn128"> + %fielded = function.call @Bridge::@value<[ + #felt> + ]>() : () -> !felt.type<"bn128"> + %other = function.call @Bridge::@value<[8]>() : () -> !felt.type<"bn128"> + function.return %fielded : !felt.type<"bn128"> + } +} + +// CHECK-LABEL: module attributes {llzk.lang} { +// CHECK-NEXT: function.def @"Seam_f<35:5:bn128>_f<7:5:bn128>_value_0"() -> !felt.type<"bn128"> { +// CHECK-NEXT: %[[VAL_0:[0-9a-zA-Z_\.]+]] = felt.const 42 : <"bn128"> +// CHECK-NEXT: function.return %[[VAL_0]] : !felt.type<"bn128"> +// CHECK-NEXT: } +// CHECK-NEXT: function.def @"Seam_f<35:5:bn128>_f<8:5:bn128>_value"() -> !felt.type<"bn128"> { +// CHECK-NEXT: %[[VAL_1:[0-9a-zA-Z_\.]+]] = felt.const 43 : <"bn128"> +// CHECK-NEXT: function.return %[[VAL_1]] : !felt.type<"bn128"> +// CHECK-NEXT: } +// CHECK-NEXT: poly.template @"Seam_f<35:5:bn128>_\1A" { +// CHECK-NEXT: poly.param @M : !felt.type<"bn128"> +// CHECK-NEXT: poly.expr @Sum { +// CHECK-NEXT: %[[VAL_2:[0-9a-zA-Z_\.]+]] = felt.const 35 : <"bn128"> +// CHECK-NEXT: %[[VAL_3:[0-9a-zA-Z_\.]+]] = poly.read_const @M : !felt.type<"bn128"> +// CHECK-NEXT: %[[VAL_4:[0-9a-zA-Z_\.]+]] = felt.add %[[VAL_3]], %[[VAL_2]] : !felt.type<"bn128">, !felt.type<"bn128"> +// CHECK-NEXT: poly.yield %[[VAL_4]] : !felt.type<"bn128"> +// CHECK-NEXT: } +// CHECK-NEXT: function.def @value() -> !felt.type<"bn128"> { +// CHECK-NEXT: %[[VAL_5:[0-9a-zA-Z_\.]+]] = poly.read_const @Sum : !felt.type<"bn128"> +// CHECK-NEXT: function.return %[[VAL_5]] : !felt.type<"bn128"> +// CHECK-NEXT: } +// CHECK-NEXT: } {poly.name_pattern = ["Seam_f<35:5:bn128>_", ""]} +// CHECK-NEXT: poly.template @Seam { +// CHECK-NEXT: poly.param @F : !felt.type<"bn128"> +// CHECK-NEXT: poly.param @M : !felt.type<"bn128"> +// CHECK-NEXT: poly.expr @Sum { +// CHECK-NEXT: %[[VAL_6:[0-9a-zA-Z_\.]+]] = poly.read_const @F : !felt.type<"bn128"> +// CHECK-NEXT: %[[VAL_7:[0-9a-zA-Z_\.]+]] = poly.read_const @M : !felt.type<"bn128"> +// CHECK-NEXT: %[[VAL_8:[0-9a-zA-Z_\.]+]] = felt.add %[[VAL_6]], %[[VAL_7]] : !felt.type<"bn128">, !felt.type<"bn128"> +// CHECK-NEXT: poly.yield %[[VAL_8]] : !felt.type<"bn128"> +// CHECK-NEXT: } +// CHECK-NEXT: function.def @value() -> !felt.type<"bn128"> { +// CHECK-NEXT: %[[VAL_9:[0-9a-zA-Z_\.]+]] = poly.read_const @Sum : !felt.type<"bn128"> +// CHECK-NEXT: function.return %[[VAL_9]] : !felt.type<"bn128"> +// CHECK-NEXT: } +// CHECK-NEXT: } +// CHECK-NEXT: function.def @"Seam_f<35:5:bn128>_f<7:5:bn128>_value"() -> !felt.type<"bn128"> { +// CHECK-NEXT: %[[VAL_10:[0-9a-zA-Z_\.]+]] = felt.const 99 : <"bn128"> +// CHECK-NEXT: function.return %[[VAL_10]] : !felt.type<"bn128"> +// CHECK-NEXT: } +// CHECK-NEXT: function.def @"Bridge_f<7:5:bn128>_value"() -> !felt.type<"bn128"> { +// CHECK-NEXT: %[[VAL_11:[0-9a-zA-Z_\.]+]] = function.call @"Seam_f<35:5:bn128>_f<7:5:bn128>_value_0"() : () -> !felt.type<"bn128"> +// CHECK-NEXT: %[[VAL_12:[0-9a-zA-Z_\.]+]] = function.call @"Seam_f<35:5:bn128>_f<7:5:bn128>_value_0"() : () -> !felt.type<"bn128"> +// CHECK-NEXT: %[[VAL_13:[0-9a-zA-Z_\.]+]] = function.call @"Seam_f<35:5:bn128>_f<7:5:bn128>_value_0"() : () -> !felt.type<"bn128"> +// CHECK-NEXT: function.return %[[VAL_13]] : !felt.type<"bn128"> +// CHECK-NEXT: } +// CHECK-NEXT: function.def @"Bridge_f<8:5:bn128>_value"() -> !felt.type<"bn128"> { +// CHECK-NEXT: %[[VAL_14:[0-9a-zA-Z_\.]+]] = function.call @"Seam_f<35:5:bn128>_f<8:5:bn128>_value"() : () -> !felt.type<"bn128"> +// CHECK-NEXT: %[[VAL_15:[0-9a-zA-Z_\.]+]] = function.call @"Seam_f<35:5:bn128>_f<8:5:bn128>_value"() : () -> !felt.type<"bn128"> +// CHECK-NEXT: %[[VAL_16:[0-9a-zA-Z_\.]+]] = function.call @"Seam_f<35:5:bn128>_f<8:5:bn128>_value"() : () -> !felt.type<"bn128"> +// CHECK-NEXT: function.return %[[VAL_16]] : !felt.type<"bn128"> +// CHECK-NEXT: } +// CHECK-NEXT: poly.template @Bridge { +// CHECK-NEXT: poly.param @M : !felt.type<"bn128"> +// CHECK-NEXT: function.def @value() -> !felt.type<"bn128"> { +// CHECK-NEXT: %[[VAL_17:[0-9a-zA-Z_\.]+]] = function.call @"Seam_f<35:5:bn128>_\1A"::@value<[@M]>() : () -> !felt.type<"bn128"> +// CHECK-NEXT: %[[VAL_18:[0-9a-zA-Z_\.]+]] = function.call @"Seam_f<35:5:bn128>_\1A"::@value<[@M]>() : () -> !felt.type<"bn128"> +// CHECK-NEXT: %[[VAL_19:[0-9a-zA-Z_\.]+]] = function.call @"Seam_f<35:5:bn128>_\1A"::@value<[@M]>() : () -> !felt.type<"bn128"> +// CHECK-NEXT: function.return %[[VAL_19]] : !felt.type<"bn128"> +// CHECK-NEXT: } +// CHECK-NEXT: } +// CHECK-NEXT: function.def @main() -> !felt.type<"bn128"> { +// CHECK-NEXT: %[[VAL_20:[0-9a-zA-Z_\.]+]] = function.call @"Bridge_f<7:5:bn128>_value"() : () -> !felt.type<"bn128"> +// CHECK-NEXT: %[[VAL_21:[0-9a-zA-Z_\.]+]] = function.call @"Bridge_f<7:5:bn128>_value"() : () -> !felt.type<"bn128"> +// CHECK-NEXT: %[[VAL_22:[0-9a-zA-Z_\.]+]] = function.call @"Bridge_f<8:5:bn128>_value"() : () -> !felt.type<"bn128"> +// CHECK-NEXT: function.return %[[VAL_21]] : !felt.type<"bn128"> +// CHECK-NEXT: } +// CHECK-NEXT: } + +// ----- + +// Distinct source paths may render the same preferred specialization name. Each sibling module +// must retain its own clone, while repeated calls within that source reuse it. +module attributes {llzk.lang} { + module @Left attributes {llzk.lang} { + poly.template @T { + poly.param @N : index + function.def @compute() -> index { + %value = poly.read_const @N : index + function.return %value : index + } + } + + function.def @main() -> index { + %first = function.call @T::@compute<[4]>() : () -> index + %second = function.call @T::@compute<[4]>() : () -> index + function.return %second : index + } + } + + module @Right attributes {llzk.lang} { + poly.template @T { + poly.param @N : index + function.def @compute() -> index { + %value = poly.read_const @N : index + function.return %value : index + } + } + + function.def @main() -> index { + %first = function.call @T::@compute<[4]>() : () -> index + %second = function.call @T::@compute<[4]>() : () -> index + function.return %second : index + } + } +} + +// CHECK-LABEL: module attributes {llzk.lang} { +// CHECK-NEXT: module @Left attributes {llzk.lang} { +// CHECK-NEXT: function.def @T_4_compute() -> index { +// CHECK-NEXT: %[[VAL_0:[0-9a-zA-Z_\.]+]] = arith.constant 4 : index +// CHECK-NEXT: function.return %[[VAL_0]] : index +// CHECK-NEXT: } +// CHECK-NEXT: poly.template @T { +// CHECK-NEXT: poly.param @N : index +// CHECK-NEXT: function.def @compute() -> index { +// CHECK-NEXT: %[[VAL_1:[0-9a-zA-Z_\.]+]] = poly.read_const @N : index +// CHECK-NEXT: function.return %[[VAL_1]] : index +// CHECK-NEXT: } +// CHECK-NEXT: } +// CHECK-NEXT: function.def @main() -> index { +// CHECK-NEXT: %[[VAL_2:[0-9a-zA-Z_\.]+]] = function.call @T_4_compute() : () -> index +// CHECK-NEXT: %[[VAL_3:[0-9a-zA-Z_\.]+]] = function.call @T_4_compute() : () -> index +// CHECK-NEXT: function.return %[[VAL_3]] : index +// CHECK-NEXT: } +// CHECK-NEXT: } +// CHECK-NEXT: module @Right attributes {llzk.lang} { +// CHECK-NEXT: function.def @T_4_compute() -> index { +// CHECK-NEXT: %[[VAL_4:[0-9a-zA-Z_\.]+]] = arith.constant 4 : index +// CHECK-NEXT: function.return %[[VAL_4]] : index +// CHECK-NEXT: } +// CHECK-NEXT: poly.template @T { +// CHECK-NEXT: poly.param @N : index +// CHECK-NEXT: function.def @compute() -> index { +// CHECK-NEXT: %[[VAL_5:[0-9a-zA-Z_\.]+]] = poly.read_const @N : index +// CHECK-NEXT: function.return %[[VAL_5]] : index +// CHECK-NEXT: } +// CHECK-NEXT: } +// CHECK-NEXT: function.def @main() -> index { +// CHECK-NEXT: %[[VAL_6:[0-9a-zA-Z_\.]+]] = function.call @T_4_compute() : () -> index +// CHECK-NEXT: %[[VAL_7:[0-9a-zA-Z_\.]+]] = function.call @T_4_compute() : () -> index +// CHECK-NEXT: function.return %[[VAL_7]] : index +// CHECK-NEXT: } +// CHECK-NEXT: } +// CHECK-NEXT: } + +// ----- + +module attributes {llzk.lang} { + // A user-defined partial template collides with the preferred name. The specialized source body + // must remain distinct, and repeated calls must use its post-insertion name. + poly.template @"T_35_\1A" { + poly.param @Ty : !poly.tvar<@Ty> + function.def @compute() -> index { + %wrong = arith.constant 99 : index + function.return %wrong : index + } + } + + poly.template @T { + poly.param @N : index + poly.param @Ty : !poly.tvar<@Ty> + function.def @compute() -> index { + %value = arith.constant 7 : index + function.return %value : index + } + + function.def @other() -> index { + %value = arith.constant 8 : index + function.return %value : index + } + } + + function.def @main() -> index { + %got = function.call @T::@compute<[35, ?]>() : () -> index + %got_again = function.call @T::@compute<[35, ?]>() : () -> index + %different_binding = function.call @T::@compute<[36, ?]>() : () -> index + %different_binding_again = function.call @T::@compute<[36, ?]>() : () -> index + %different_source = function.call @T::@other<[35, ?]>() : () -> index + %different_source_again = function.call @T::@other<[35, ?]>() : () -> index + function.return %different_source_again : index + } +} + +// CHECK-LABEL: module attributes {llzk.lang} { +// CHECK-NEXT: poly.template @"T_35_\1A" { +// CHECK-NEXT: poly.param @Ty : !poly.tvar<@Ty> +// CHECK-NEXT: function.def @compute() -> index { +// CHECK-NEXT: %[[VAL_0:[0-9a-zA-Z_\.]+]] = arith.constant 99 : index +// CHECK-NEXT: function.return %[[VAL_0]] : index +// CHECK-NEXT: } +// CHECK-NEXT: } +// CHECK-NEXT: poly.template @"T_35_\1A_0" { +// CHECK-NEXT: poly.param @Ty : !poly.tvar<@Ty> +// CHECK-NEXT: function.def @compute() -> index { +// CHECK-NEXT: %[[VAL_1:[0-9a-zA-Z_\.]+]] = arith.constant 7 : index +// CHECK-NEXT: function.return %[[VAL_1]] : index +// CHECK-NEXT: } +// CHECK-NEXT: } {poly.name_pattern = ["T_35_", ""]} +// CHECK-NEXT: poly.template @"T_36_\1A" { +// CHECK-NEXT: poly.param @Ty : !poly.tvar<@Ty> +// CHECK-NEXT: function.def @compute() -> index { +// CHECK-NEXT: %[[VAL_2:[0-9a-zA-Z_\.]+]] = arith.constant 7 : index +// CHECK-NEXT: function.return %[[VAL_2]] : index +// CHECK-NEXT: } +// CHECK-NEXT: } {poly.name_pattern = ["T_36_", ""]} +// CHECK-NEXT: poly.template @"T_35_\1A_1" { +// CHECK-NEXT: poly.param @Ty : !poly.tvar<@Ty> +// CHECK-NEXT: function.def @other() -> index { +// CHECK-NEXT: %[[VAL_3:[0-9a-zA-Z_\.]+]] = arith.constant 8 : index +// CHECK-NEXT: function.return %[[VAL_3]] : index +// CHECK-NEXT: } +// CHECK-NEXT: } {poly.name_pattern = ["T_35_", ""]} +// CHECK-NEXT: poly.template @T { +// CHECK-NEXT: poly.param @N : index +// CHECK-NEXT: poly.param @Ty : !poly.tvar<@Ty> +// CHECK-NEXT: function.def @compute() -> index { +// CHECK-NEXT: %[[VAL_4:[0-9a-zA-Z_\.]+]] = arith.constant 7 : index +// CHECK-NEXT: function.return %[[VAL_4]] : index +// CHECK-NEXT: } +// CHECK-NEXT: function.def @other() -> index { +// CHECK-NEXT: %[[VAL_5:[0-9a-zA-Z_\.]+]] = arith.constant 8 : index +// CHECK-NEXT: function.return %[[VAL_5]] : index +// CHECK-NEXT: } +// CHECK-NEXT: } +// CHECK-NEXT: function.def @main() -> index { +// CHECK-NEXT: %[[VAL_6:[0-9a-zA-Z_\.]+]] = function.call @"T_35_\1A_0"::@compute<[?]>() : () -> index +// CHECK-NEXT: %[[VAL_7:[0-9a-zA-Z_\.]+]] = function.call @"T_35_\1A_0"::@compute<[?]>() : () -> index +// CHECK-NEXT: %[[VAL_8:[0-9a-zA-Z_\.]+]] = function.call @"T_36_\1A"::@compute<[?]>() : () -> index +// CHECK-NEXT: %[[VAL_9:[0-9a-zA-Z_\.]+]] = function.call @"T_36_\1A"::@compute<[?]>() : () -> index +// CHECK-NEXT: %[[VAL_10:[0-9a-zA-Z_\.]+]] = function.call @"T_35_\1A_1"::@other<[?]>() : () -> index +// CHECK-NEXT: %[[VAL_11:[0-9a-zA-Z_\.]+]] = function.call @"T_35_\1A_1"::@other<[?]>() : () -> index +// CHECK-NEXT: function.return %[[VAL_11]] : index +// CHECK-NEXT: } +// CHECK-NEXT: } diff --git a/test/Transforms/Flattening/instantiate_nested_call_template_params.llzk b/test/Transforms/Flattening/instantiate_nested_call_template_params.llzk new file mode 100644 index 0000000000..42703e5fbd --- /dev/null +++ b/test/Transforms/Flattening/instantiate_nested_call_template_params.llzk @@ -0,0 +1,142 @@ +// RUN: llzk-opt -llzk-flatten=cleanup=disabled %s | FileCheck %s + +// Explicit nested-call arguments must retain their order and recursively materialize type +// variables after the enclosing specialization supplies the final binding. +module attributes {llzk.lang} { + poly.template @NestedArgBox { + poly.param @Elem : !poly.tvar<@Elem> + + struct.def @Box { + struct.member @value : !poly.tvar<@Elem> + + function.def @product( + %value: !poly.tvar<@Elem> + ) -> !struct.type<@NestedArgBox::@Box<[@Elem]>> { + %self = struct.new : !struct.type<@NestedArgBox::@Box<[@Elem]>> + struct.writem %self[@value] = %value : + !struct.type<@NestedArgBox::@Box<[@Elem]>>, !poly.tvar<@Elem> + function.return %self : !struct.type<@NestedArgBox::@Box<[@Elem]>> + } + } + } + + poly.template @NestedArgInner { + poly.param @Marker : index + poly.param @ArrayTy : !poly.tvar<@ArrayTy> + poly.param @StructTy : !poly.tvar<@StructTy> + poly.param @PodTy : !poly.tvar<@PodTy> + poly.param @ArgTy : !poly.tvar<@ArgTy> + + function.def @apply(%arg: !poly.tvar<@ArgTy>) -> index { + %marker = poly.read_const @Marker : index + function.return %marker : index + } + } + + poly.template @NestedArgOuter { + poly.param @N : index + poly.param @T : !poly.tvar<@T> + poly.param @Marker : index + + function.def @apply(%arg: !poly.tvar<@T>) -> index { + %marker = function.call @NestedArgInner::@apply<[ + @Marker, + !array.type<@N x !poly.tvar<@T>>, + !struct.type<@NestedArgBox::@Box<[@T]>>, + !pod.type<[@payload: !poly.tvar<@T>]>, + !poly.tvar<@T> + ]>(%arg) : (!poly.tvar<@T>) -> index + function.return %marker : index + } + } + + poly.template @NestedArgBridge { + poly.param @T : !poly.tvar<@T> + + function.def @apply(%arg: !poly.tvar<@T>) -> index { + %marker = function.call @NestedArgOuter::@apply<[4, @T, 41]>(%arg) : + (!poly.tvar<@T>) -> index + function.return %marker : index + } + } + + function.def @main(%arg: !felt.type<"bn128">) -> index { + %marker = function.call @NestedArgBridge::@apply<[!felt.type<"bn128">]>(%arg) : + (!felt.type<"bn128">) -> index + function.return %marker : index + } +} + +// CHECK-LABEL: module attributes {llzk.lang} { +// CHECK-NEXT: poly.template @NestedArgBox { +// CHECK-NEXT: poly.param @Elem : !poly.tvar<@Elem> +// CHECK-NEXT: struct.def @Box { +// CHECK-NEXT: struct.member @value : !poly.tvar<@Elem> +// CHECK-NEXT: function.def @product(%[[VAL_0:[0-9a-zA-Z_\.]+]]: !poly.tvar<@Elem>) -> !struct.type<@NestedArgBox::@Box<[@Elem]>> attributes {function.allow_constraint, function.allow_witness} { +// CHECK-NEXT: %[[VAL_1:[0-9a-zA-Z_\.]+]] = struct.new : <@NestedArgBox::@Box<[@Elem]>> +// CHECK-NEXT: struct.writem %[[VAL_1]][@value] = %[[VAL_0]] : <@NestedArgBox::@Box<[@Elem]>>, !poly.tvar<@Elem> +// CHECK-NEXT: function.return %[[VAL_1]] : !struct.type<@NestedArgBox::@Box<[@Elem]>> +// CHECK-NEXT: } +// CHECK-NEXT: } +// CHECK-NEXT: } +// CHECK-NEXT: function.def @"NestedArgInner_41_!a_!s<@NestedArgBox::@Box_f>_!r<@payload>_f_apply"(%[[VAL_2:[0-9a-zA-Z_\.]+]]: !felt.type<"bn128">) -> index { +// CHECK-NEXT: %[[VAL_3:[0-9a-zA-Z_\.]+]] = arith.constant 41 : index +// CHECK-NEXT: function.return %[[VAL_3]] : index +// CHECK-NEXT: } +// CHECK-NEXT: poly.template @"NestedArgInner_41_\1A_\1A_\1A_\1A" { +// CHECK-NEXT: poly.param @ArrayTy : !poly.tvar<@ArrayTy> +// CHECK-NEXT: poly.param @StructTy : !poly.tvar<@StructTy> +// CHECK-NEXT: poly.param @PodTy : !poly.tvar<@PodTy> +// CHECK-NEXT: poly.param @ArgTy : !poly.tvar<@ArgTy> +// CHECK-NEXT: function.def @apply(%[[VAL_4:[0-9a-zA-Z_\.]+]]: !poly.tvar<@ArgTy>) -> index { +// CHECK-NEXT: %[[VAL_5:[0-9a-zA-Z_\.]+]] = arith.constant 41 : index +// CHECK-NEXT: function.return %[[VAL_5]] : index +// CHECK-NEXT: } +// CHECK-NEXT: } {poly.name_pattern = ["NestedArgInner_41_", "_", "_", "_", ""]} +// CHECK-NEXT: poly.template @NestedArgInner { +// CHECK-NEXT: poly.param @Marker : index +// CHECK-NEXT: poly.param @ArrayTy : !poly.tvar<@ArrayTy> +// CHECK-NEXT: poly.param @StructTy : !poly.tvar<@StructTy> +// CHECK-NEXT: poly.param @PodTy : !poly.tvar<@PodTy> +// CHECK-NEXT: poly.param @ArgTy : !poly.tvar<@ArgTy> +// CHECK-NEXT: function.def @apply(%[[VAL_6:[0-9a-zA-Z_\.]+]]: !poly.tvar<@ArgTy>) -> index { +// CHECK-NEXT: %[[VAL_7:[0-9a-zA-Z_\.]+]] = poly.read_const @Marker : index +// CHECK-NEXT: function.return %[[VAL_7]] : index +// CHECK-NEXT: } +// CHECK-NEXT: } +// CHECK-NEXT: function.def @NestedArgOuter_4_f_41_apply(%[[VAL_8:[0-9a-zA-Z_\.]+]]: !felt.type<"bn128">) -> index { +// CHECK-NEXT: %[[VAL_9:[0-9a-zA-Z_\.]+]] = function.call @"NestedArgInner_41_!a_!s<@NestedArgBox::@Box_f>_!r<@payload>_f_apply"(%[[VAL_8]]) : (!felt.type<"bn128">) -> index +// CHECK-NEXT: function.return %[[VAL_9]] : index +// CHECK-NEXT: } +// CHECK-NEXT: poly.template @"NestedArgOuter_4_\1A_41" { +// CHECK-NEXT: poly.param @T : !poly.tvar<@T> +// CHECK-NEXT: function.def @apply(%[[VAL_10:[0-9a-zA-Z_\.]+]]: !poly.tvar<@T>) -> index { +// CHECK-NEXT: %[[VAL_11:[0-9a-zA-Z_\.]+]] = function.call @"NestedArgInner_41_\1A_\1A_\1A_\1A"::@apply<[!array.type<4 x !poly.tvar<@T>>, !struct.type<@NestedArgBox::@Box<[@T]>>, !pod.type<[@payload: !poly.tvar<@T>]>, !poly.tvar<@T>]>(%[[VAL_10]]) : (!poly.tvar<@T>) -> index +// CHECK-NEXT: function.return %[[VAL_11]] : index +// CHECK-NEXT: } +// CHECK-NEXT: } {poly.name_pattern = ["NestedArgOuter_4_", "_41"]} +// CHECK-NEXT: poly.template @NestedArgOuter { +// CHECK-NEXT: poly.param @N : index +// CHECK-NEXT: poly.param @T : !poly.tvar<@T> +// CHECK-NEXT: poly.param @Marker : index +// CHECK-NEXT: function.def @apply(%[[VAL_12:[0-9a-zA-Z_\.]+]]: !poly.tvar<@T>) -> index { +// CHECK-NEXT: %[[VAL_13:[0-9a-zA-Z_\.]+]] = function.call @NestedArgInner::@apply<[@Marker, !array.type<@N x !poly.tvar<@T>>, !struct.type<@NestedArgBox::@Box<[@T]>>, !pod.type<[@payload: !poly.tvar<@T>]>, !poly.tvar<@T>]>(%[[VAL_12]]) : (!poly.tvar<@T>) -> index +// CHECK-NEXT: function.return %[[VAL_13]] : index +// CHECK-NEXT: } +// CHECK-NEXT: } +// CHECK-NEXT: function.def @NestedArgBridge_f_apply(%[[VAL_14:[0-9a-zA-Z_\.]+]]: !felt.type<"bn128">) -> index { +// CHECK-NEXT: %[[VAL_15:[0-9a-zA-Z_\.]+]] = function.call @NestedArgOuter_4_f_41_apply(%[[VAL_14]]) : (!felt.type<"bn128">) -> index +// CHECK-NEXT: function.return %[[VAL_15]] : index +// CHECK-NEXT: } +// CHECK-NEXT: poly.template @NestedArgBridge { +// CHECK-NEXT: poly.param @T : !poly.tvar<@T> +// CHECK-NEXT: function.def @apply(%[[VAL_16:[0-9a-zA-Z_\.]+]]: !poly.tvar<@T>) -> index { +// CHECK-NEXT: %[[VAL_17:[0-9a-zA-Z_\.]+]] = function.call @"NestedArgOuter_4_\1A_41"::@apply<[@T]>(%[[VAL_16]]) : (!poly.tvar<@T>) -> index +// CHECK-NEXT: function.return %[[VAL_17]] : index +// CHECK-NEXT: } +// CHECK-NEXT: } +// CHECK-NEXT: function.def @main(%[[VAL_18:[0-9a-zA-Z_\.]+]]: !felt.type<"bn128">) -> index { +// CHECK-NEXT: %[[VAL_19:[0-9a-zA-Z_\.]+]] = function.call @NestedArgBridge_f_apply(%[[VAL_18]]) : (!felt.type<"bn128">) -> index +// CHECK-NEXT: function.return %[[VAL_19]] : index +// CHECK-NEXT: } +// CHECK-NEXT: } diff --git a/test/Transforms/Flattening/instantiate_struct_rollback.llzk b/test/Transforms/Flattening/instantiate_struct_rollback.llzk new file mode 100644 index 0000000000..9cf949863f --- /dev/null +++ b/test/Transforms/Flattening/instantiate_struct_rollback.llzk @@ -0,0 +1,136 @@ +// RUN: llzk-opt -split-input-file -llzk-flatten -verify-diagnostics %s 2>&1 | FileCheck --enable-var-scope %s + +module attributes {llzk.lang} { + poly.template @BrokenStruct { + poly.param @Num + + struct.def @Value { + struct.member @value : !felt.type<"bn128"> {column, llzk.pub} + + function.def @compute() -> !struct.type<@BrokenStruct::@Value<[@Num]>> { + // expected-error@+2 {{felt constant}} + // expected-error@+1 {{failed to legalize operation 'poly.read_const'}} + %value = poly.read_const @Num : !felt.type<"bn128"> + %self = struct.new : !struct.type<@BrokenStruct::@Value<[@Num]>> + struct.writem %self[@value] = %value : !struct.type<@BrokenStruct::@Value<[@Num]>>, !felt.type<"bn128"> + function.return %self : !struct.type<@BrokenStruct::@Value<[@Num]>> + } + + function.def @constrain(%self: !struct.type<@BrokenStruct::@Value<[@Num]>>) { + function.return + } + } + } + + struct.def @Use { + struct.member @inner : !struct.type<@BrokenStruct::@Value<[#felt>]>> + + function.def @compute() -> !struct.type<@Use> { + %self = struct.new : !struct.type<@Use> + function.return %self : !struct.type<@Use> + } + + function.def @constrain(%self: !struct.type<@Use>) { + function.return + } + } +} + +// A failed generated-struct conversion must not leave its inserted owner in the module. +// CHECK-LABEL: module attributes {llzk.lang} { +// CHECK-NOT: BrokenStruct_f< +// CHECK: struct.def @Use { + +// ----- + +module attributes {llzk.lang} { + poly.template @BrokenPartial { + poly.param @Num + poly.param @Offset : index + + struct.def @Value { + struct.member @value : !felt.type<"bn128"> {column, llzk.pub} + + function.def @compute() -> !struct.type<@BrokenPartial::@Value<[@Num, @Offset]>> { + // expected-error@+2 {{felt constant}} + // expected-error@+1 {{failed to legalize operation 'poly.read_const'}} + %value = poly.read_const @Num : !felt.type<"bn128"> + %self = struct.new : !struct.type<@BrokenPartial::@Value<[@Num, @Offset]>> + struct.writem %self[@value] = %value : !struct.type<@BrokenPartial::@Value<[@Num, @Offset]>>, !felt.type<"bn128"> + function.return %self : !struct.type<@BrokenPartial::@Value<[@Num, @Offset]>> + } + + function.def @constrain(%self: !struct.type<@BrokenPartial::@Value<[@Num, @Offset]>>) { + function.return + } + } + } + + poly.template @Bridge { + poly.param @Offset : index + + struct.def @Wrapper { + struct.member @inner : !struct.type<@BrokenPartial::@Value<[#felt>, @Offset]>> + + function.def @compute() -> !struct.type<@Bridge::@Wrapper<[@Offset]>> { + %self = struct.new : !struct.type<@Bridge::@Wrapper<[@Offset]>> + function.return %self : !struct.type<@Bridge::@Wrapper<[@Offset]>> + } + + function.def @constrain(%self: !struct.type<@Bridge::@Wrapper<[@Offset]>>) { + function.return + } + } + } + + struct.def @BadUse { + struct.member @inner : !struct.type<@Bridge::@Wrapper<[7]>> + + function.def @compute() -> !struct.type<@BadUse> { + %self = struct.new : !struct.type<@BadUse> + function.return %self : !struct.type<@BadUse> + } + + function.def @constrain(%self: !struct.type<@BadUse>) { + function.return + } + } + + // This specialization runs after the failed partial specialization above in the same pass. + poly.template @GoodStruct { + poly.param @N : index + + struct.def @Value { + struct.member @value : index + + function.def @compute() -> !struct.type<@GoodStruct::@Value<[@N]>> { + %self = struct.new : !struct.type<@GoodStruct::@Value<[@N]>> + function.return %self : !struct.type<@GoodStruct::@Value<[@N]>> + } + + function.def @constrain(%self: !struct.type<@GoodStruct::@Value<[@N]>>) { + function.return + } + } + } + + struct.def @GoodUse { + struct.member @inner : !struct.type<@GoodStruct::@Value<[7]>> + + function.def @compute() -> !struct.type<@GoodUse> { + %self = struct.new : !struct.type<@GoodUse> + function.return %self : !struct.type<@GoodUse> + } + + function.def @constrain(%self: !struct.type<@GoodUse>) { + function.return + } + } +} + +// A failed partial conversion must not leave its generated template or its cached symbol table. +// The later specialization proves that the same cloner remains usable after rollback. +// CHECK-LABEL: module attributes {llzk.lang} { +// CHECK-NOT: BrokenPartial_f< +// CHECK: struct.def @GoodStruct_7_Value { +// CHECK: struct.def @GoodUse { diff --git a/test/Transforms/Flattening/instantiate_structs_pass_3.llzk b/test/Transforms/Flattening/instantiate_structs_pass_3.llzk index 20d2c26e85..0accf6b77d 100644 --- a/test/Transforms/Flattening/instantiate_structs_pass_3.llzk +++ b/test/Transforms/Flattening/instantiate_structs_pass_3.llzk @@ -46,39 +46,40 @@ module attributes {llzk.lang} { } } } -// CHECK-LABEL: struct.def @T1_43_MyStruct { -// CHECK-NEXT: function.def @compute() -> !struct.type<@T1_43_MyStruct> attributes {function.allow_witness} { -// CHECK-NEXT: %[[VAL_0:[0-9a-zA-Z_\.]+]] = struct.new : <@T1_43_MyStruct> -// CHECK-NEXT: function.return %[[VAL_0]] : !struct.type<@T1_43_MyStruct> -// CHECK-NEXT: } -// CHECK-NEXT: function.def @constrain(%[[VAL_1:[0-9a-zA-Z_\.]+]]: !struct.type<@T1_43_MyStruct>) attributes {function.allow_constraint} { -// CHECK-NEXT: function.return -// CHECK-NEXT: } -// CHECK-NEXT: } -// -// CHECK-LABEL: struct.def @T2_43_MyStruct { -// CHECK-NEXT: function.def @compute() -> !struct.type<@T2_43_MyStruct> attributes {function.allow_witness} { -// CHECK-NEXT: %[[VAL_2:[0-9a-zA-Z_\.]+]] = struct.new : <@T2_43_MyStruct> -// CHECK-NEXT: function.return %[[VAL_2]] : !struct.type<@T2_43_MyStruct> -// CHECK-NEXT: } -// CHECK-NEXT: function.def @constrain(%[[VAL_3:[0-9a-zA-Z_\.]+]]: !struct.type<@T2_43_MyStruct>) attributes {function.allow_constraint} { -// CHECK-NEXT: function.return + +// CHECK-LABEL: module attributes {llzk.lang} { +// CHECK-NEXT: struct.def @T1_43_MyStruct { +// CHECK-NEXT: function.def @compute() -> !struct.type<@T1_43_MyStruct> attributes {function.allow_witness} { +// CHECK-NEXT: %[[VAL_0:[0-9a-zA-Z_\.]+]] = struct.new : <@T1_43_MyStruct> +// CHECK-NEXT: function.return %[[VAL_0]] : !struct.type<@T1_43_MyStruct> +// CHECK-NEXT: } +// CHECK-NEXT: function.def @constrain(%[[VAL_1:[0-9a-zA-Z_\.]+]]: !struct.type<@T1_43_MyStruct>) attributes {function.allow_constraint} { +// CHECK-NEXT: function.return +// CHECK-NEXT: } // CHECK-NEXT: } -// CHECK-NEXT: } -// -// CHECK-LABEL: struct.def @Main { -// CHECK-NEXT: struct.member @f1 : !struct.type<@T1_43_MyStruct> -// CHECK-NEXT: struct.member @f2 : !struct.type<@T2_43_MyStruct> -// CHECK-NEXT: function.def @compute() -> !struct.type<@Main> attributes {function.allow_witness} { -// CHECK-NEXT: %[[VAL_4:[0-9a-zA-Z_\.]+]] = struct.new : <@Main> -// CHECK-NEXT: %[[VAL_5:[0-9a-zA-Z_\.]+]] = function.call @T1_43_MyStruct::@compute() : () -> !struct.type<@T1_43_MyStruct> -// CHECK-NEXT: struct.writem %[[VAL_4]][@f1] = %[[VAL_5]] : <@Main>, !struct.type<@T1_43_MyStruct> -// CHECK-NEXT: %[[VAL_6:[0-9a-zA-Z_\.]+]] = function.call @T2_43_MyStruct::@compute() : () -> !struct.type<@T2_43_MyStruct> -// CHECK-NEXT: struct.writem %[[VAL_4]][@f2] = %[[VAL_6]] : <@Main>, !struct.type<@T2_43_MyStruct> -// CHECK-NEXT: function.return %[[VAL_4]] : !struct.type<@Main> +// CHECK-NEXT: struct.def @T2_43_MyStruct { +// CHECK-NEXT: function.def @compute() -> !struct.type<@T2_43_MyStruct> attributes {function.allow_witness} { +// CHECK-NEXT: %[[VAL_2:[0-9a-zA-Z_\.]+]] = struct.new : <@T2_43_MyStruct> +// CHECK-NEXT: function.return %[[VAL_2]] : !struct.type<@T2_43_MyStruct> +// CHECK-NEXT: } +// CHECK-NEXT: function.def @constrain(%[[VAL_3:[0-9a-zA-Z_\.]+]]: !struct.type<@T2_43_MyStruct>) attributes {function.allow_constraint} { +// CHECK-NEXT: function.return +// CHECK-NEXT: } // CHECK-NEXT: } -// CHECK-NEXT: function.def @constrain(%[[VAL_7:[0-9a-zA-Z_\.]+]]: !struct.type<@Main>) attributes {function.allow_constraint} { -// CHECK-NEXT: function.return +// CHECK-NEXT: struct.def @Main { +// CHECK-NEXT: struct.member @f1 : !struct.type<@T1_43_MyStruct> +// CHECK-NEXT: struct.member @f2 : !struct.type<@T2_43_MyStruct> +// CHECK-NEXT: function.def @compute() -> !struct.type<@Main> attributes {function.allow_witness} { +// CHECK-NEXT: %[[VAL_4:[0-9a-zA-Z_\.]+]] = struct.new : <@Main> +// CHECK-NEXT: %[[VAL_5:[0-9a-zA-Z_\.]+]] = function.call @T1_43_MyStruct::@compute() : () -> !struct.type<@T1_43_MyStruct> +// CHECK-NEXT: struct.writem %[[VAL_4]][@f1] = %[[VAL_5]] : <@Main>, !struct.type<@T1_43_MyStruct> +// CHECK-NEXT: %[[VAL_6:[0-9a-zA-Z_\.]+]] = function.call @T2_43_MyStruct::@compute() : () -> !struct.type<@T2_43_MyStruct> +// CHECK-NEXT: struct.writem %[[VAL_4]][@f2] = %[[VAL_6]] : <@Main>, !struct.type<@T2_43_MyStruct> +// CHECK-NEXT: function.return %[[VAL_4]] : !struct.type<@Main> +// CHECK-NEXT: } +// CHECK-NEXT: function.def @constrain(%[[VAL_7:[0-9a-zA-Z_\.]+]]: !struct.type<@Main>) attributes {function.allow_constraint} { +// CHECK-NEXT: function.return +// CHECK-NEXT: } // CHECK-NEXT: } // CHECK-NEXT: } // ----- @@ -125,39 +126,40 @@ module attributes {llzk.lang} { } } } -// CHECK-LABEL: struct.def @MyTemplate_78_Struct1 { -// CHECK-NEXT: function.def @compute() -> !struct.type<@MyTemplate_78_Struct1> attributes {function.allow_witness} { -// CHECK-NEXT: %[[VAL_0:[0-9a-zA-Z_\.]+]] = struct.new : <@MyTemplate_78_Struct1> -// CHECK-NEXT: function.return %[[VAL_0]] : !struct.type<@MyTemplate_78_Struct1> -// CHECK-NEXT: } -// CHECK-NEXT: function.def @constrain(%[[VAL_1:[0-9a-zA-Z_\.]+]]: !struct.type<@MyTemplate_78_Struct1>) attributes {function.allow_constraint} { -// CHECK-NEXT: function.return -// CHECK-NEXT: } -// CHECK-NEXT: } -// -// CHECK-LABEL: struct.def @MyTemplate_13_Struct2 { -// CHECK-NEXT: function.def @compute() -> !struct.type<@MyTemplate_13_Struct2> attributes {function.allow_witness} { -// CHECK-NEXT: %[[VAL_2:[0-9a-zA-Z_\.]+]] = struct.new : <@MyTemplate_13_Struct2> -// CHECK-NEXT: function.return %[[VAL_2]] : !struct.type<@MyTemplate_13_Struct2> -// CHECK-NEXT: } -// CHECK-NEXT: function.def @constrain(%[[VAL_3:[0-9a-zA-Z_\.]+]]: !struct.type<@MyTemplate_13_Struct2>) attributes {function.allow_constraint} { -// CHECK-NEXT: function.return + +// CHECK-LABEL: module attributes {llzk.lang} { +// CHECK-NEXT: struct.def @MyTemplate_78_Struct1 { +// CHECK-NEXT: function.def @compute() -> !struct.type<@MyTemplate_78_Struct1> attributes {function.allow_witness} { +// CHECK-NEXT: %[[VAL_0:[0-9a-zA-Z_\.]+]] = struct.new : <@MyTemplate_78_Struct1> +// CHECK-NEXT: function.return %[[VAL_0]] : !struct.type<@MyTemplate_78_Struct1> +// CHECK-NEXT: } +// CHECK-NEXT: function.def @constrain(%[[VAL_1:[0-9a-zA-Z_\.]+]]: !struct.type<@MyTemplate_78_Struct1>) attributes {function.allow_constraint} { +// CHECK-NEXT: function.return +// CHECK-NEXT: } // CHECK-NEXT: } -// CHECK-NEXT: } -// -// CHECK-LABEL: struct.def @Main { -// CHECK-NEXT: struct.member @f1 : !struct.type<@MyTemplate_78_Struct1> -// CHECK-NEXT: struct.member @f2 : !struct.type<@MyTemplate_13_Struct2> -// CHECK-NEXT: function.def @compute() -> !struct.type<@Main> attributes {function.allow_witness} { -// CHECK-NEXT: %[[VAL_4:[0-9a-zA-Z_\.]+]] = struct.new : <@Main> -// CHECK-NEXT: %[[VAL_5:[0-9a-zA-Z_\.]+]] = function.call @MyTemplate_78_Struct1::@compute() : () -> !struct.type<@MyTemplate_78_Struct1> -// CHECK-NEXT: struct.writem %[[VAL_4]][@f1] = %[[VAL_5]] : <@Main>, !struct.type<@MyTemplate_78_Struct1> -// CHECK-NEXT: %[[VAL_6:[0-9a-zA-Z_\.]+]] = function.call @MyTemplate_13_Struct2::@compute() : () -> !struct.type<@MyTemplate_13_Struct2> -// CHECK-NEXT: struct.writem %[[VAL_4]][@f2] = %[[VAL_6]] : <@Main>, !struct.type<@MyTemplate_13_Struct2> -// CHECK-NEXT: function.return %[[VAL_4]] : !struct.type<@Main> +// CHECK-NEXT: struct.def @MyTemplate_13_Struct2 { +// CHECK-NEXT: function.def @compute() -> !struct.type<@MyTemplate_13_Struct2> attributes {function.allow_witness} { +// CHECK-NEXT: %[[VAL_2:[0-9a-zA-Z_\.]+]] = struct.new : <@MyTemplate_13_Struct2> +// CHECK-NEXT: function.return %[[VAL_2]] : !struct.type<@MyTemplate_13_Struct2> +// CHECK-NEXT: } +// CHECK-NEXT: function.def @constrain(%[[VAL_3:[0-9a-zA-Z_\.]+]]: !struct.type<@MyTemplate_13_Struct2>) attributes {function.allow_constraint} { +// CHECK-NEXT: function.return +// CHECK-NEXT: } // CHECK-NEXT: } -// CHECK-NEXT: function.def @constrain(%[[VAL_7:[0-9a-zA-Z_\.]+]]: !struct.type<@Main>) attributes {function.allow_constraint} { -// CHECK-NEXT: function.return +// CHECK-NEXT: struct.def @Main { +// CHECK-NEXT: struct.member @f1 : !struct.type<@MyTemplate_78_Struct1> +// CHECK-NEXT: struct.member @f2 : !struct.type<@MyTemplate_13_Struct2> +// CHECK-NEXT: function.def @compute() -> !struct.type<@Main> attributes {function.allow_witness} { +// CHECK-NEXT: %[[VAL_4:[0-9a-zA-Z_\.]+]] = struct.new : <@Main> +// CHECK-NEXT: %[[VAL_5:[0-9a-zA-Z_\.]+]] = function.call @MyTemplate_78_Struct1::@compute() : () -> !struct.type<@MyTemplate_78_Struct1> +// CHECK-NEXT: struct.writem %[[VAL_4]][@f1] = %[[VAL_5]] : <@Main>, !struct.type<@MyTemplate_78_Struct1> +// CHECK-NEXT: %[[VAL_6:[0-9a-zA-Z_\.]+]] = function.call @MyTemplate_13_Struct2::@compute() : () -> !struct.type<@MyTemplate_13_Struct2> +// CHECK-NEXT: struct.writem %[[VAL_4]][@f2] = %[[VAL_6]] : <@Main>, !struct.type<@MyTemplate_13_Struct2> +// CHECK-NEXT: function.return %[[VAL_4]] : !struct.type<@Main> +// CHECK-NEXT: } +// CHECK-NEXT: function.def @constrain(%[[VAL_7:[0-9a-zA-Z_\.]+]]: !struct.type<@Main>) attributes {function.allow_constraint} { +// CHECK-NEXT: function.return +// CHECK-NEXT: } // CHECK-NEXT: } // CHECK-NEXT: } // ----- @@ -182,19 +184,21 @@ module attributes {llzk.lang} { function.def @constrain(%self: !struct.type<@Main>) { function.return } } } -// CHECK-LABEL: function.def @NeedsTemplateParam_89_id() -> index { -// CHECK-NEXT: %[[VAL_0:[0-9a-zA-Z_\.]+]] = arith.constant 89 : index -// CHECK-NEXT: function.return %[[VAL_0]] : index -// CHECK-NEXT: } -// -// CHECK-LABEL: struct.def @Main { -// CHECK-NEXT: function.def @compute() -> !struct.type<@Main> attributes {function.allow_witness} { -// CHECK-NEXT: %[[VAL_2:[0-9a-zA-Z_\.]+]] = struct.new : <@Main> -// CHECK-NEXT: %[[VAL_3:[0-9a-zA-Z_\.]+]] = function.call @NeedsTemplateParam_89_id() : () -> index -// CHECK-NEXT: function.return %[[VAL_2]] : !struct.type<@Main> + +// CHECK-LABEL: module attributes {llzk.lang} { +// CHECK-NEXT: function.def @NeedsTemplateParam_89_id() -> index { +// CHECK-NEXT: %[[VAL_0:[0-9a-zA-Z_\.]+]] = arith.constant 89 : index +// CHECK-NEXT: function.return %[[VAL_0]] : index // CHECK-NEXT: } -// CHECK-NEXT: function.def @constrain(%[[VAL_4:[0-9a-zA-Z_\.]+]]: !struct.type<@Main>) attributes {function.allow_constraint} { -// CHECK-NEXT: function.return +// CHECK-NEXT: struct.def @Main { +// CHECK-NEXT: function.def @compute() -> !struct.type<@Main> attributes {function.allow_witness} { +// CHECK-NEXT: %[[VAL_1:[0-9a-zA-Z_\.]+]] = struct.new : <@Main> +// CHECK-NEXT: %[[VAL_2:[0-9a-zA-Z_\.]+]] = function.call @NeedsTemplateParam_89_id() : () -> index +// CHECK-NEXT: function.return %[[VAL_1]] : !struct.type<@Main> +// CHECK-NEXT: } +// CHECK-NEXT: function.def @constrain(%[[VAL_3:[0-9a-zA-Z_\.]+]]: !struct.type<@Main>) attributes {function.allow_constraint} { +// CHECK-NEXT: function.return +// CHECK-NEXT: } // CHECK-NEXT: } // CHECK-NEXT: } // ----- @@ -242,45 +246,46 @@ module attributes {llzk.lang} { } } } -// CHECK-LABEL: struct.def @TemplateA_56_9_StructA { -// CHECK-NEXT: struct.member @f1 : !felt.type<"goldilocks"> -// CHECK-NEXT: function.def @compute() -> !struct.type<@TemplateA_56_9_StructA> attributes {function.allow_witness} { -// CHECK-NEXT: %[[VAL_0:[0-9a-zA-Z_\.]+]] = felt.const 65 -// CHECK-NEXT: %[[VAL_1:[0-9a-zA-Z_\.]+]] = struct.new : <@TemplateA_56_9_StructA> -// CHECK-NEXT: struct.writem %[[VAL_1]][@f1] = %[[VAL_0]] : <@TemplateA_56_9_StructA>, !felt.type<"goldilocks"> -// CHECK-NEXT: function.return %[[VAL_1]] : !struct.type<@TemplateA_56_9_StructA> -// CHECK-NEXT: } -// CHECK-NEXT: function.def @constrain(%[[VAL_2:[0-9a-zA-Z_\.]+]]: !struct.type<@TemplateA_56_9_StructA>) attributes {function.allow_constraint} { -// CHECK-NEXT: function.return -// CHECK-NEXT: } -// CHECK-NEXT: } -// -// CHECK-LABEL: struct.def @TemplateA_8_25_StructA { -// CHECK-NEXT: struct.member @f1 : !felt.type<"goldilocks"> -// CHECK-NEXT: function.def @compute() -> !struct.type<@TemplateA_8_25_StructA> attributes {function.allow_witness} { -// CHECK-NEXT: %[[VAL_3:[0-9a-zA-Z_\.]+]] = felt.const 33 -// CHECK-NEXT: %[[VAL_4:[0-9a-zA-Z_\.]+]] = struct.new : <@TemplateA_8_25_StructA> -// CHECK-NEXT: struct.writem %[[VAL_4]][@f1] = %[[VAL_3]] : <@TemplateA_8_25_StructA>, !felt.type<"goldilocks"> -// CHECK-NEXT: function.return %[[VAL_4]] : !struct.type<@TemplateA_8_25_StructA> -// CHECK-NEXT: } -// CHECK-NEXT: function.def @constrain(%[[VAL_5:[0-9a-zA-Z_\.]+]]: !struct.type<@TemplateA_8_25_StructA>) attributes {function.allow_constraint} { -// CHECK-NEXT: function.return + +// CHECK-LABEL: module attributes {llzk.lang} { +// CHECK-NEXT: struct.def @"TemplateA_f<56:10:goldilocks>_f<9:10:goldilocks>_StructA" { +// CHECK-NEXT: struct.member @f1 : !felt.type<"goldilocks"> +// CHECK-NEXT: function.def @compute() -> !struct.type<@"TemplateA_f<56:10:goldilocks>_f<9:10:goldilocks>_StructA"> attributes {function.allow_witness} { +// CHECK-NEXT: %[[VAL_0:[0-9a-zA-Z_\.]+]] = felt.const 65 : <"goldilocks"> +// CHECK-NEXT: %[[VAL_1:[0-9a-zA-Z_\.]+]] = struct.new : <@"TemplateA_f<56:10:goldilocks>_f<9:10:goldilocks>_StructA"> +// CHECK-NEXT: struct.writem %[[VAL_1]][@f1] = %[[VAL_0]] : <@"TemplateA_f<56:10:goldilocks>_f<9:10:goldilocks>_StructA">, !felt.type<"goldilocks"> +// CHECK-NEXT: function.return %[[VAL_1]] : !struct.type<@"TemplateA_f<56:10:goldilocks>_f<9:10:goldilocks>_StructA"> +// CHECK-NEXT: } +// CHECK-NEXT: function.def @constrain(%[[VAL_2:[0-9a-zA-Z_\.]+]]: !struct.type<@"TemplateA_f<56:10:goldilocks>_f<9:10:goldilocks>_StructA">) attributes {function.allow_constraint} { +// CHECK-NEXT: function.return +// CHECK-NEXT: } // CHECK-NEXT: } -// CHECK-NEXT: } -// -// CHECK-LABEL: struct.def @Main { -// CHECK-NEXT: struct.member @f1 : !struct.type<@TemplateA_56_9_StructA> -// CHECK-NEXT: struct.member @f2 : !struct.type<@TemplateA_8_25_StructA> -// CHECK-NEXT: function.def @compute() -> !struct.type<@Main> attributes {function.allow_witness} { -// CHECK-NEXT: %[[VAL_4:[0-9a-zA-Z_\.]+]] = struct.new : <@Main> -// CHECK-NEXT: %[[VAL_5:[0-9a-zA-Z_\.]+]] = function.call @TemplateA_56_9_StructA::@compute() : () -> !struct.type<@TemplateA_56_9_StructA> -// CHECK-NEXT: struct.writem %[[VAL_4]][@f1] = %[[VAL_5]] : <@Main>, !struct.type<@TemplateA_56_9_StructA> -// CHECK-NEXT: %[[VAL_6:[0-9a-zA-Z_\.]+]] = function.call @TemplateA_8_25_StructA::@compute() : () -> !struct.type<@TemplateA_8_25_StructA> -// CHECK-NEXT: struct.writem %[[VAL_4]][@f2] = %[[VAL_6]] : <@Main>, !struct.type<@TemplateA_8_25_StructA> -// CHECK-NEXT: function.return %[[VAL_4]] : !struct.type<@Main> +// CHECK-NEXT: struct.def @"TemplateA_f<8:10:goldilocks>_f<25:10:goldilocks>_StructA" { +// CHECK-NEXT: struct.member @f1 : !felt.type<"goldilocks"> +// CHECK-NEXT: function.def @compute() -> !struct.type<@"TemplateA_f<8:10:goldilocks>_f<25:10:goldilocks>_StructA"> attributes {function.allow_witness} { +// CHECK-NEXT: %[[VAL_3:[0-9a-zA-Z_\.]+]] = felt.const 33 : <"goldilocks"> +// CHECK-NEXT: %[[VAL_4:[0-9a-zA-Z_\.]+]] = struct.new : <@"TemplateA_f<8:10:goldilocks>_f<25:10:goldilocks>_StructA"> +// CHECK-NEXT: struct.writem %[[VAL_4]][@f1] = %[[VAL_3]] : <@"TemplateA_f<8:10:goldilocks>_f<25:10:goldilocks>_StructA">, !felt.type<"goldilocks"> +// CHECK-NEXT: function.return %[[VAL_4]] : !struct.type<@"TemplateA_f<8:10:goldilocks>_f<25:10:goldilocks>_StructA"> +// CHECK-NEXT: } +// CHECK-NEXT: function.def @constrain(%[[VAL_5:[0-9a-zA-Z_\.]+]]: !struct.type<@"TemplateA_f<8:10:goldilocks>_f<25:10:goldilocks>_StructA">) attributes {function.allow_constraint} { +// CHECK-NEXT: function.return +// CHECK-NEXT: } // CHECK-NEXT: } -// CHECK-NEXT: function.def @constrain(%[[VAL_7:[0-9a-zA-Z_\.]+]]: !struct.type<@Main>) attributes {function.allow_constraint} { -// CHECK-NEXT: function.return +// CHECK-NEXT: struct.def @Main { +// CHECK-NEXT: struct.member @f1 : !struct.type<@"TemplateA_f<56:10:goldilocks>_f<9:10:goldilocks>_StructA"> +// CHECK-NEXT: struct.member @f2 : !struct.type<@"TemplateA_f<8:10:goldilocks>_f<25:10:goldilocks>_StructA"> +// CHECK-NEXT: function.def @compute() -> !struct.type<@Main> attributes {function.allow_witness} { +// CHECK-NEXT: %[[VAL_6:[0-9a-zA-Z_\.]+]] = struct.new : <@Main> +// CHECK-NEXT: %[[VAL_7:[0-9a-zA-Z_\.]+]] = function.call @"TemplateA_f<56:10:goldilocks>_f<9:10:goldilocks>_StructA"::@compute() : () -> !struct.type<@"TemplateA_f<56:10:goldilocks>_f<9:10:goldilocks>_StructA"> +// CHECK-NEXT: struct.writem %[[VAL_6]][@f1] = %[[VAL_7]] : <@Main>, !struct.type<@"TemplateA_f<56:10:goldilocks>_f<9:10:goldilocks>_StructA"> +// CHECK-NEXT: %[[VAL_8:[0-9a-zA-Z_\.]+]] = function.call @"TemplateA_f<8:10:goldilocks>_f<25:10:goldilocks>_StructA"::@compute() : () -> !struct.type<@"TemplateA_f<8:10:goldilocks>_f<25:10:goldilocks>_StructA"> +// CHECK-NEXT: struct.writem %[[VAL_6]][@f2] = %[[VAL_8]] : <@Main>, !struct.type<@"TemplateA_f<8:10:goldilocks>_f<25:10:goldilocks>_StructA"> +// CHECK-NEXT: function.return %[[VAL_6]] : !struct.type<@Main> +// CHECK-NEXT: } +// CHECK-NEXT: function.def @constrain(%[[VAL_9:[0-9a-zA-Z_\.]+]]: !struct.type<@Main>) attributes {function.allow_constraint} { +// CHECK-NEXT: function.return +// CHECK-NEXT: } // CHECK-NEXT: } // CHECK-NEXT: } // ----- @@ -334,28 +339,29 @@ module attributes {llzk.lang} { } } } + // CHECK-LABEL: module attributes {llzk.lang} { -// CHECK-NEXT: struct.def @TemplateB_4_3_StructB { +// CHECK-NEXT: struct.def @"TemplateB_f<4:5:bn254>_f<3:5:bn254>_StructB" { // CHECK-NEXT: struct.member @sum : !felt.type<"bn254"> // CHECK-NEXT: struct.member @prod : !felt.type<"bn254"> -// CHECK-NEXT: function.def @compute() -> !struct.type<@TemplateB_4_3_StructB> attributes {function.allow_witness} { -// CHECK-NEXT: %[[VAL_0:[0-9a-zA-Z_\.]+]] = felt.const 12 -// CHECK-NEXT: %[[VAL_1:[0-9a-zA-Z_\.]+]] = felt.const 7 -// CHECK-NEXT: %[[VAL_2:[0-9a-zA-Z_\.]+]] = struct.new : <@TemplateB_4_3_StructB> -// CHECK-NEXT: struct.writem %[[VAL_2]][@sum] = %[[VAL_1]] : <@TemplateB_4_3_StructB>, !felt.type<"bn254"> -// CHECK-NEXT: struct.writem %[[VAL_2]][@prod] = %[[VAL_0]] : <@TemplateB_4_3_StructB>, !felt.type<"bn254"> -// CHECK-NEXT: function.return %[[VAL_2]] : !struct.type<@TemplateB_4_3_StructB> +// CHECK-NEXT: function.def @compute() -> !struct.type<@"TemplateB_f<4:5:bn254>_f<3:5:bn254>_StructB"> attributes {function.allow_witness} { +// CHECK-NEXT: %[[VAL_0:[0-9a-zA-Z_\.]+]] = felt.const 12 : <"bn254"> +// CHECK-NEXT: %[[VAL_1:[0-9a-zA-Z_\.]+]] = felt.const 7 : <"bn254"> +// CHECK-NEXT: %[[VAL_2:[0-9a-zA-Z_\.]+]] = struct.new : <@"TemplateB_f<4:5:bn254>_f<3:5:bn254>_StructB"> +// CHECK-NEXT: struct.writem %[[VAL_2]][@sum] = %[[VAL_1]] : <@"TemplateB_f<4:5:bn254>_f<3:5:bn254>_StructB">, !felt.type<"bn254"> +// CHECK-NEXT: struct.writem %[[VAL_2]][@prod] = %[[VAL_0]] : <@"TemplateB_f<4:5:bn254>_f<3:5:bn254>_StructB">, !felt.type<"bn254"> +// CHECK-NEXT: function.return %[[VAL_2]] : !struct.type<@"TemplateB_f<4:5:bn254>_f<3:5:bn254>_StructB"> // CHECK-NEXT: } -// CHECK-NEXT: function.def @constrain(%[[VAL_3:[0-9a-zA-Z_\.]+]]: !struct.type<@TemplateB_4_3_StructB>) attributes {function.allow_constraint} { +// CHECK-NEXT: function.def @constrain(%[[VAL_3:[0-9a-zA-Z_\.]+]]: !struct.type<@"TemplateB_f<4:5:bn254>_f<3:5:bn254>_StructB">) attributes {function.allow_constraint} { // CHECK-NEXT: function.return // CHECK-NEXT: } // CHECK-NEXT: } // CHECK-NEXT: struct.def @Main { -// CHECK-NEXT: struct.member @f1 : !struct.type<@TemplateB_4_3_StructB> +// CHECK-NEXT: struct.member @f1 : !struct.type<@"TemplateB_f<4:5:bn254>_f<3:5:bn254>_StructB"> // CHECK-NEXT: function.def @compute() -> !struct.type<@Main> attributes {function.allow_witness} { // CHECK-NEXT: %[[VAL_4:[0-9a-zA-Z_\.]+]] = struct.new : <@Main> -// CHECK-NEXT: %[[VAL_5:[0-9a-zA-Z_\.]+]] = function.call @TemplateB_4_3_StructB::@compute() : () -> !struct.type<@TemplateB_4_3_StructB> -// CHECK-NEXT: struct.writem %[[VAL_4]][@f1] = %[[VAL_5]] : <@Main>, !struct.type<@TemplateB_4_3_StructB> +// CHECK-NEXT: %[[VAL_5:[0-9a-zA-Z_\.]+]] = function.call @"TemplateB_f<4:5:bn254>_f<3:5:bn254>_StructB"::@compute() : () -> !struct.type<@"TemplateB_f<4:5:bn254>_f<3:5:bn254>_StructB"> +// CHECK-NEXT: struct.writem %[[VAL_4]][@f1] = %[[VAL_5]] : <@Main>, !struct.type<@"TemplateB_f<4:5:bn254>_f<3:5:bn254>_StructB"> // CHECK-NEXT: function.return %[[VAL_4]] : !struct.type<@Main> // CHECK-NEXT: } // CHECK-NEXT: function.def @constrain(%[[VAL_6:[0-9a-zA-Z_\.]+]]: !struct.type<@Main>) attributes {function.allow_constraint} { @@ -392,19 +398,20 @@ module attributes {llzk.lang} { } } } -// CHECK-LABEL: function.def @template_expr_A_5_f( -// CHECK-SAME: %[[VAL_0:[0-9a-zA-Z_\.]+]]: !array.type<5 x !felt.type>) -> index { -// CHECK-NEXT: %[[VAL_1:[0-9a-zA-Z_\.]+]] = arith.constant 10 : index -// CHECK-NEXT: function.return %[[VAL_1]] : index -// CHECK-NEXT: } -// -// CHECK-LABEL: struct.def @Main { -// CHECK-NEXT: function.def @compute(%[[VAL_8:[0-9a-zA-Z_\.]+]]: !array.type<5 x !felt.type>) -> !struct.type<@Main> attributes {function.allow_witness} { -// CHECK-NEXT: %[[VAL_9:[0-9a-zA-Z_\.]+]] = struct.new : <@Main> -// CHECK-NEXT: %[[VAL_10:[0-9a-zA-Z_\.]+]] = function.call @template_expr_A_5_f(%[[VAL_8]]) : (!array.type<5 x !felt.type>) -> index -// CHECK-NEXT: function.return %[[VAL_9]] : !struct.type<@Main> + +// CHECK-LABEL: module attributes {llzk.lang} { +// CHECK-NEXT: function.def @template_expr_A_5_f(%[[VAL_0:[0-9a-zA-Z_\.]+]]: !array.type<5 x !felt.type>) -> index { +// CHECK-NEXT: %[[VAL_1:[0-9a-zA-Z_\.]+]] = arith.constant 10 : index +// CHECK-NEXT: function.return %[[VAL_1]] : index // CHECK-NEXT: } -// CHECK-NEXT: function.def @constrain(%[[VAL_11:[0-9a-zA-Z_\.]+]]: !struct.type<@Main>, %[[VAL_12:[0-9a-zA-Z_\.]+]]: !array.type<5 x !felt.type>) attributes {function.allow_constraint} { -// CHECK-NEXT: function.return +// CHECK-NEXT: struct.def @Main { +// CHECK-NEXT: function.def @compute(%[[VAL_2:[0-9a-zA-Z_\.]+]]: !array.type<5 x !felt.type>) -> !struct.type<@Main> attributes {function.allow_witness} { +// CHECK-NEXT: %[[VAL_3:[0-9a-zA-Z_\.]+]] = struct.new : <@Main> +// CHECK-NEXT: %[[VAL_4:[0-9a-zA-Z_\.]+]] = function.call @template_expr_A_5_f(%[[VAL_2]]) : (!array.type<5 x !felt.type>) -> index +// CHECK-NEXT: function.return %[[VAL_3]] : !struct.type<@Main> +// CHECK-NEXT: } +// CHECK-NEXT: function.def @constrain(%[[VAL_5:[0-9a-zA-Z_\.]+]]: !struct.type<@Main>, %[[VAL_6:[0-9a-zA-Z_\.]+]]: !array.type<5 x !felt.type>) attributes {function.allow_constraint} { +// CHECK-NEXT: function.return +// CHECK-NEXT: } // CHECK-NEXT: } // CHECK-NEXT: } diff --git a/test/Transforms/TypeVarInference/infer_tvars_fail.llzk b/test/Transforms/TypeVarInference/infer_tvars_fail.llzk index 53a3dc55c4..4d40958f3b 100644 --- a/test/Transforms/TypeVarInference/infer_tvars_fail.llzk +++ b/test/Transforms/TypeVarInference/infer_tvars_fail.llzk @@ -256,7 +256,7 @@ module attributes {llzk.lang} { poly.param @N : index function.def @forward(%arg: !felt.type) -> !felt.type { - // expected-error@+1 {{explicit template argument for inferred parameter @T must match inferred type '!felt.type', but found @N}} + // expected-error@+1 {{'function.call' op instantiation value '@N' is not compatible with parameter "@T" type restriction '!poly.tvar<@T>'}} %0 = function.call @TExplicitForeignSymbolConflictCallee::@f<[@N]>(%arg) : (!felt.type) -> !felt.type function.return %0 : !felt.type } diff --git a/unittests/IR/FeltFoldTests.cpp b/unittests/IR/FeltFoldTests.cpp index b510078cb7..da740cb7d2 100644 --- a/unittests/IR/FeltFoldTests.cpp +++ b/unittests/IR/FeltFoldTests.cpp @@ -94,6 +94,22 @@ class BabyBearFoldTest : public LLZKTest { void expectNoFold(FeltConstAttr result) { EXPECT_FALSE(result) << "expected fold to be skipped"; } }; +TEST_F(BabyBearFoldTest, MaterializeConstantType) { + FeltType babyBear = FeltType::get(&ctx, BB_FIELD); + FeltType goldilocks = FeltType::get(&ctx, "goldilocks"); + + FailureOr adopted = unspecifiedConst(5).materializeAs(babyBear); + ASSERT_TRUE(succeeded(adopted)); + EXPECT_EQ(adopted->getType(), babyBear); + + FailureOr matching = babyBearConst(5).materializeAs(babyBear); + ASSERT_TRUE(succeeded(matching)); + EXPECT_EQ(matching->getType(), babyBear); + + EXPECT_EQ(babyBearConst(5).materializeAs(FeltType::get(&ctx)).value().getType(), babyBear); + EXPECT_TRUE(failed(babyBearConst(5).materializeAs(goldilocks))); +} + //===------------------------------------------------------------------===// // felt.add //===------------------------------------------------------------------===// diff --git a/unittests/IR/FunctionDialectTests.cpp b/unittests/IR/FunctionDialectTests.cpp index 604d1086f9..b5a70055b6 100644 --- a/unittests/IR/FunctionDialectTests.cpp +++ b/unittests/IR/FunctionDialectTests.cpp @@ -14,6 +14,8 @@ #include "llzk/Dialect/Shared/Builders.h" #include +#include +#include using namespace mlir; using namespace llzk; @@ -193,6 +195,165 @@ TEST_F(OpTests, testCallNoAffine_InvalidTemplateParam) { ); } +TEST_F(OpTests, testCallFeltRestrictionAcceptsFieldedLocalSymbol) { + constexpr StringLiteral source = R"mlir( +module attributes {llzk.lang} { + poly.template @BoxTemplate { + poly.param @P + struct.def @Box { + function.def @compute() -> !struct.type<@BoxTemplate::@Box<[@P]>> { + %self = struct.new : <@BoxTemplate::@Box<[@P]>> + function.return %self : !struct.type<@BoxTemplate::@Box<[@P]>> + } + function.def @constrain(%self: !struct.type<@BoxTemplate::@Box<[@P]>>) { + function.return + } + } + } + + poly.template @Target { + poly.param @F : !felt.type + function.def @accept(%value: !struct.type<@BoxTemplate::@Box<[@F]>>) { + function.return + } + } + + poly.template @Caller { + poly.param @G : !felt.type<"bn128"> + function.def @caller(%value: !struct.type<@BoxTemplate::@Box<[@G]>>) { + function.call @Target::@accept<[@G]>(%value) : + (!struct.type<@BoxTemplate::@Box<[@G]>>) -> () + function.call @Target::@accept(%value) : + (!struct.type<@BoxTemplate::@Box<[@G]>>) -> () + function.return + } + } +} +)mlir"; + + auto parsed = parseSourceString(source, ParserConfig(&ctx)); + ASSERT_TRUE(parsed); + SmallVector calls; + parsed->walk([&calls](CallOp op) { calls.push_back(op); }); + + ASSERT_EQ(calls.size(), 2u); + ASSERT_TRUE(succeeded(mlir::verify(parsed.get()))); + EXPECT_TRUE(verify(calls[0], true)); + EXPECT_TRUE(verify(calls[1], true)); +} + +TEST_F(OpTests, testCallFeltRestrictionNormalizesEquivalentConstants) { + constexpr StringLiteral source = R"mlir( +module attributes {llzk.lang} { + poly.template @BoxTemplate { + poly.param @P + struct.def @Box { + function.def @compute() -> !struct.type<@BoxTemplate::@Box<[@P]>> { + %self = struct.new : <@BoxTemplate::@Box<[@P]>> + function.return %self : !struct.type<@BoxTemplate::@Box<[@P]>> + } + function.def @constrain(%self: !struct.type<@BoxTemplate::@Box<[@P]>>) { + function.return + } + } + } + + poly.template @Target { + poly.param @F : !felt.type<"bn128"> + function.def @accept(%value: !struct.type<@BoxTemplate::@Box<[@F]>>) { + function.return + } + } + + poly.template @Caller { + function.def @caller( + %value: !struct.type<@BoxTemplate::@Box<[#felt>]>> + ) { + function.call @Target::@accept<[#felt]>(%value) : + (!struct.type<@BoxTemplate::@Box<[#felt>]>>) -> () + function.call @Target::@accept<[35]>(%value) : + (!struct.type<@BoxTemplate::@Box<[#felt>]>>) -> () + function.return + } + } +} +)mlir"; + + auto parsed = parseSourceString(source, ParserConfig(&ctx)); + ASSERT_TRUE(parsed); + SmallVector calls; + parsed->walk([&calls](CallOp op) { calls.push_back(op); }); + + ASSERT_EQ(calls.size(), 2u); + ASSERT_TRUE(succeeded(mlir::verify(parsed.get()))); + EXPECT_TRUE(verify(calls[0], true)); + EXPECT_TRUE(verify(calls[1], true)); +} + +TEST_F(OpTests, testCallFeltRestrictionRejectsIncompatibleLocalSymbols) { + constexpr StringLiteral source = R"mlir( +module attributes {llzk.lang} { + poly.template @BoxTemplate { + poly.param @P + struct.def @Box { + function.def @compute() -> !struct.type<@BoxTemplate::@Box<[@P]>> { + %self = struct.new : <@BoxTemplate::@Box<[@P]>> + function.return %self : !struct.type<@BoxTemplate::@Box<[@P]>> + } + function.def @constrain(%self: !struct.type<@BoxTemplate::@Box<[@P]>>) { + function.return + } + } + } + + poly.template @Target { + poly.param @F : !felt.type<"bn128"> + function.def @accept(%value: !struct.type<@BoxTemplate::@Box<[@F]>>) { + function.return + } + } + + poly.template @FieldlessCaller { + poly.param @G : !felt.type + function.def @caller(%value: !struct.type<@BoxTemplate::@Box<[@G]>>) { + function.return + } + } + + poly.template @MismatchedCaller { + poly.param @G : !felt.type<"goldilocks"> + function.def @caller(%value: !struct.type<@BoxTemplate::@Box<[@G]>>) { + function.return + } + } +} +)mlir"; + + auto parsed = parseSourceString(source, ParserConfig(&ctx)); + ASSERT_TRUE(parsed); + ASSERT_TRUE(succeeded(mlir::verify(parsed.get()))); + + auto callee = SymbolRefAttr::get( + &ctx, "Target", ArrayRef {FlatSymbolRefAttr::get(&ctx, "accept")} + ); + auto param = FlatSymbolRefAttr::get(&ctx, "G"); + SmallVector calls; + parsed->walk([&](FuncDefOp func) { + if (func.getSymName() != "caller") { + return; + } + Block &body = func.getBody().front(); + OpBuilder builder(&body, body.getTerminator()->getIterator()); + calls.push_back(builder.create( + loc, TypeRange {}, callee, ValueRange {body.getArgument(0)}, ArrayRef {param} + )); + }); + + ASSERT_EQ(calls.size(), 2u); + EXPECT_FALSE(verify(calls[0], true)); + EXPECT_FALSE(verify(calls[1], true)); +} + //===------------------------------------------------------------------===// // CallOp::build(..., TypeRange, SymbolRefAttr, ArrayRef, // ArrayRef, ValueRange = {}) diff --git a/unittests/IR/VerifDialectTests.cpp b/unittests/IR/VerifDialectTests.cpp index 64a59ded6d..e4c254b8f5 100644 --- a/unittests/IR/VerifDialectTests.cpp +++ b/unittests/IR/VerifDialectTests.cpp @@ -219,6 +219,199 @@ module attributes {llzk.lang} { ASSERT_TRUE(verify(includes.front(), true)); } +TEST_F(VerifDialectTests, TemplateIncludeAcceptsFieldedLocalSymbolForFieldlessRestriction) { + constexpr StringLiteral source = R"mlir( +module attributes {llzk.lang} { + poly.template @BoxTemplate { + poly.param @P + struct.def @Box { + function.def @compute() -> !struct.type<@BoxTemplate::@Box<[@P]>> { + %self = struct.new : <@BoxTemplate::@Box<[@P]>> + function.return %self : !struct.type<@BoxTemplate::@Box<[@P]>> + } + function.def @constrain(%self: !struct.type<@BoxTemplate::@Box<[@P]>>) { + function.return + } + } + } + + poly.template @Target { + poly.param @F : !felt.type + function.def @accept(%value: !struct.type<@BoxTemplate::@Box<[@F]>>) { + function.return + } + verif.contract @Base for @Target::@accept ( + %value: !struct.type<@BoxTemplate::@Box<[@F]>> + ) { + %ok = arith.constant true + verif.ensure_compute %ok + } + } + + poly.template @Caller { + poly.param @G : !felt.type<"bn128"> + function.def @caller(%value: !struct.type<@BoxTemplate::@Box<[@G]>>) { + function.return + } + verif.contract @Wrapper for @Caller::@caller ( + %value: !struct.type<@BoxTemplate::@Box<[@G]>> + ) { + verif.include @Target::@Base<[@G]>(%value) : + (!struct.type<@BoxTemplate::@Box<[@G]>>) -> () + verif.include @Target::@Base(%value) : + (!struct.type<@BoxTemplate::@Box<[@G]>>) -> () + } + } +} +)mlir"; + + auto parsed = parseModule(source); + auto includes = findOps(*parsed); + + ASSERT_EQ(includes.size(), 2u); + ASSERT_TRUE(succeeded(mlir::verify(parsed.get()))); + EXPECT_TRUE(verify(includes[0], true)); + EXPECT_TRUE(verify(includes[1], true)); +} + +TEST_F(VerifDialectTests, IncludeFeltRestrictionNormalizesEquivalentConstants) { + constexpr StringLiteral source = R"mlir( +module attributes {llzk.lang} { + poly.template @BoxTemplate { + poly.param @P + struct.def @Box { + function.def @compute() -> !struct.type<@BoxTemplate::@Box<[@P]>> { + %self = struct.new : <@BoxTemplate::@Box<[@P]>> + function.return %self : !struct.type<@BoxTemplate::@Box<[@P]>> + } + function.def @constrain(%self: !struct.type<@BoxTemplate::@Box<[@P]>>) { + function.return + } + } + } + + poly.template @Target { + poly.param @F : !felt.type<"bn128"> + function.def @accept(%value: !struct.type<@BoxTemplate::@Box<[@F]>>) { + function.return + } + verif.contract @Base for @Target::@accept ( + %value: !struct.type<@BoxTemplate::@Box<[@F]>> + ) { + %ok = arith.constant true + verif.ensure_compute %ok + } + } + + poly.template @Caller { + function.def @caller( + %value: !struct.type<@BoxTemplate::@Box<[#felt>]>> + ) { + function.return + } + verif.contract @Wrapper for @Caller::@caller ( + %value: !struct.type<@BoxTemplate::@Box<[#felt>]>> + ) { + verif.include @Target::@Base<[#felt]>(%value) : + (!struct.type<@BoxTemplate::@Box<[#felt>]>>) -> () + verif.include @Target::@Base<[35]>(%value) : + (!struct.type<@BoxTemplate::@Box<[#felt>]>>) -> () + } + } +} +)mlir"; + + auto parsed = parseModule(source); + auto includes = findOps(*parsed); + + ASSERT_EQ(includes.size(), 2u); + ASSERT_TRUE(succeeded(mlir::verify(parsed.get()))); + EXPECT_TRUE(verify(includes[0], true)); + EXPECT_TRUE(verify(includes[1], true)); +} + +TEST_F(VerifDialectTests, TemplateIncludeRejectsIncompatibleLocalSymbols) { + constexpr StringLiteral source = R"mlir( +module attributes {llzk.lang} { + poly.template @BoxTemplate { + poly.param @P + struct.def @Box { + function.def @compute() -> !struct.type<@BoxTemplate::@Box<[@P]>> { + %self = struct.new : <@BoxTemplate::@Box<[@P]>> + function.return %self : !struct.type<@BoxTemplate::@Box<[@P]>> + } + function.def @constrain(%self: !struct.type<@BoxTemplate::@Box<[@P]>>) { + function.return + } + } + } + + poly.template @Target { + poly.param @F : !felt.type<"bn128"> + function.def @accept(%value: !struct.type<@BoxTemplate::@Box<[@F]>>) { + function.return + } + verif.contract @Base for @Target::@accept ( + %value: !struct.type<@BoxTemplate::@Box<[@F]>> + ) { + %ok = arith.constant true + verif.ensure_compute %ok + } + } + + poly.template @FieldlessCaller { + poly.param @G : !felt.type + function.def @caller(%value: !struct.type<@BoxTemplate::@Box<[@G]>>) { + function.return + } + verif.contract @Wrapper for @FieldlessCaller::@caller ( + %value: !struct.type<@BoxTemplate::@Box<[@G]>> + ) { + %ok = arith.constant true + verif.ensure_compute %ok + } + } + + poly.template @MismatchedCaller { + poly.param @G : !felt.type<"goldilocks"> + function.def @caller(%value: !struct.type<@BoxTemplate::@Box<[@G]>>) { + function.return + } + verif.contract @Wrapper for @MismatchedCaller::@caller ( + %value: !struct.type<@BoxTemplate::@Box<[@G]>> + ) { + %ok = arith.constant true + verif.ensure_compute %ok + } + } +} +)mlir"; + + auto parsed = parseModule(source); + ASSERT_TRUE(parsed); + ASSERT_TRUE(succeeded(mlir::verify(parsed.get()))); + + auto callee = SymbolRefAttr::get( + &ctx, "Target", ArrayRef {FlatSymbolRefAttr::get(&ctx, "Base")} + ); + auto param = FlatSymbolRefAttr::get(&ctx, "G"); + SmallVector includes; + parsed->walk([&](ContractOp contract) { + if (contract.getSymName() != "Wrapper") { + return; + } + Block &body = contract.getBody().front(); + OpBuilder builder(&body, body.getTerminator()->getIterator()); + includes.push_back(builder.create( + loc, callee, ValueRange {body.getArgument(0)}, ArrayRef {param} + )); + }); + + ASSERT_EQ(includes.size(), 2u); + EXPECT_FALSE(verify(includes[0], true)); + EXPECT_FALSE(verify(includes[1], true)); +} + TEST_F(VerifDialectTests, ContractOutsideTemplateCanReadTargetFunctionTemplateConstants) { constexpr StringLiteral source = R"mlir( module attributes {llzk.lang} { diff --git a/unittests/Util/TypeHelperTest.cpp b/unittests/Util/TypeHelperTest.cpp index 3d31a3fe3c..62427239a5 100644 --- a/unittests/Util/TypeHelperTest.cpp +++ b/unittests/Util/TypeHelperTest.cpp @@ -12,6 +12,8 @@ #include "../LLZKTestBase.h" #include "llzk/Dialect/Array/IR/Types.h" +#include "llzk/Dialect/Felt/IR/Attrs.h" +#include "llzk/Dialect/Felt/IR/Types.h" #include "llzk/Dialect/POD/IR/Types.h" #include "llzk/Dialect/Struct/IR/Types.h" @@ -23,6 +25,7 @@ using namespace mlir; using namespace llzk; using namespace llzk::array; using namespace llzk::component; +using namespace llzk::felt; using namespace llzk::pod; class TypeHelperTests : public LLZKTest { @@ -103,6 +106,64 @@ TEST_F(TypeHelperTests, test_functionTypesUnify_Output_Fail) { ASSERT_FALSE(functionTypesUnify(a, b)); } +TEST_F(TypeHelperTests, test_templateParamTypeCompatibility_feltFields) { + FeltType fieldless = FeltType::get(&ctx); + FeltType bn128 = FeltType::get(&ctx, "bn128"); + FeltType goldilocks = FeltType::get(&ctx, "goldilocks"); + + ASSERT_TRUE(isTemplateParamTypeCompatible(bn128, fieldless)); + ASSERT_TRUE(isTemplateParamTypeCompatible(fieldless, fieldless)); + ASSERT_FALSE(isTemplateParamTypeCompatible(fieldless, bn128)); + ASSERT_TRUE(isTemplateParamTypeCompatible(bn128, bn128)); + ASSERT_FALSE(isTemplateParamTypeCompatible(goldilocks, bn128)); + ASSERT_FALSE(isTemplateParamTypeCompatible(IndexType::get(&ctx), bn128)); + ASSERT_FALSE(isTemplateParamTypeCompatible(std::nullopt, bn128)); +} + +TEST_F(TypeHelperTests, test_templateParamValuesUnify_feltRepresentations) { + FeltType fieldless = FeltType::get(&ctx); + FeltType bn128 = FeltType::get(&ctx, "bn128"); + FeltType goldilocks = FeltType::get(&ctx, "goldilocks"); + FeltConstAttr unspecified = FeltConstAttr::get(&ctx, APInt(8, 35), fieldless); + FeltConstAttr fielded = FeltConstAttr::get(&ctx, APInt(8, 35), bn128); + FeltConstAttr differentValue = FeltConstAttr::get(&ctx, APInt(8, 36), bn128); + FeltConstAttr differentField = FeltConstAttr::get(&ctx, APInt(8, 35), goldilocks); + IntegerAttr integer = IntegerAttr::get(IndexType::get(&ctx), 35); + FlatSymbolRefAttr actualSymbol = FlatSymbolRefAttr::get(&ctx, "Actual"); + FlatSymbolRefAttr inferredSymbol = FlatSymbolRefAttr::get(&ctx, "Inferred"); + + EXPECT_TRUE(templateParamValuesUnify(unspecified, fielded, fieldless)); + EXPECT_TRUE(templateParamValuesUnify(unspecified, fielded, bn128)); + EXPECT_TRUE(templateParamValuesUnify(integer, fielded, bn128)); + EXPECT_TRUE(templateParamValuesUnify(fielded, integer, bn128)); + EXPECT_FALSE(templateParamValuesUnify(differentValue, fielded, bn128)); + EXPECT_FALSE(templateParamValuesUnify(differentField, fielded, fieldless)); + EXPECT_FALSE(templateParamValuesUnify(differentField, differentField, bn128)); + EXPECT_TRUE(templateParamValuesUnify(actualSymbol, inferredSymbol, bn128)); +} + +TEST_F(TypeHelperTests, test_templateParamValuesUnify_widthIndependentFeltValues) { + static constexpr unsigned NARROW_WIDTH = 8; + static constexpr unsigned WIDE_WIDTH = 64; + static constexpr unsigned VALUE = 35; + + FeltType fieldless = FeltType::get(&ctx); + FeltType bn128 = FeltType::get(&ctx, "bn128"); + FeltType goldilocks = FeltType::get(&ctx, "goldilocks"); + IntegerAttr wideInteger = IntegerAttr::get(IntegerType::get(&ctx, WIDE_WIDTH), VALUE); + IntegerAttr differentWideInteger = + IntegerAttr::get(IntegerType::get(&ctx, WIDE_WIDTH), VALUE + 1); + FeltConstAttr narrowBn128 = FeltConstAttr::get(&ctx, APInt(NARROW_WIDTH, VALUE), bn128); + FeltConstAttr wideBn128 = FeltConstAttr::get(&ctx, APInt(WIDE_WIDTH, VALUE), bn128); + FeltConstAttr narrowGoldilocks = FeltConstAttr::get(&ctx, APInt(NARROW_WIDTH, VALUE), goldilocks); + + EXPECT_TRUE(templateParamValuesUnify(wideInteger, narrowBn128, fieldless)); + EXPECT_TRUE(templateParamValuesUnify(narrowBn128, wideInteger, fieldless)); + EXPECT_FALSE(templateParamValuesUnify(differentWideInteger, narrowBn128, fieldless)); + EXPECT_TRUE(templateParamValuesUnify(wideBn128, narrowBn128, fieldless)); + EXPECT_FALSE(templateParamValuesUnify(wideBn128, narrowGoldilocks, fieldless)); +} + TEST_F(TypeHelperTests, test_forceIntToIndexType_fromI1) { // create a boolean IntegerAttr IntegerAttr a = IntegerAttr::get(IntegerType::get(&ctx, 1), 1);