//===- BasicpyOps.td - Basic Python ops --------------------*- tablegen -*-===// // // This file is licensed under the Apache License v2.0 with LLVM Exceptions. // See https://llvm.org/LICENSE.txt for license information. // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception // //===----------------------------------------------------------------------===// #ifndef NPCOMP_DIALECT_BASICPY_IR_BASICPY_OPS #define NPCOMP_DIALECT_BASICPY_IR_BASICPY_OPS include "npcomp/Dialect/Basicpy/IR/BasicpyDialect.td" include "mlir/Interfaces/CallInterfaces.td" include "mlir/Interfaces/ControlFlowInterfaces.td" include "mlir/Interfaces/SideEffectInterfaces.td" include "mlir/IR/OpAsmInterface.td" include "mlir/IR/SymbolInterfaces.td" //===----------------------------------------------------------------------===// // Predicates //===----------------------------------------------------------------------===// def BoolOrI1Type : AnyTypeOf<[Basicpy_BoolType, I1], "Python bool or i1">; //===----------------------------------------------------------------------===// // Binary operation enum // The name matches the operation name in the python AST ("Add", "Mult", etc). //===----------------------------------------------------------------------===// def BINOP_ADD : StrEnumAttrCase<"Add">; def BINOP_BITAND : StrEnumAttrCase<"BitAnd">; def BINOP_BITOR : StrEnumAttrCase<"BitOr">; def BINOP_BITXOR : StrEnumAttrCase<"BitXor">; def BINOP_DIV : StrEnumAttrCase<"Div">; def BINOP_FLOORDIV : StrEnumAttrCase<"FloorDiv">; def BINOP_LSHIFT : StrEnumAttrCase<"LShift">; def BINOP_MATMULT : StrEnumAttrCase<"MatMult">; def BINOP_MOD : StrEnumAttrCase<"Mod">; def BINOP_MULT : StrEnumAttrCase<"Mult">; def BINOP_RSHIFT : StrEnumAttrCase<"RShift">; def BINOP_SUB : StrEnumAttrCase<"Sub">; def BinaryOperationAttr : StrEnumAttr< "BinaryOperation", "Operation for a binary expression", [ BINOP_ADD, BINOP_BITAND, BINOP_BITOR, BINOP_BITXOR, BINOP_DIV, BINOP_FLOORDIV, BINOP_LSHIFT, BINOP_MATMULT, BINOP_MOD, BINOP_MULT, BINOP_RSHIFT, BINOP_SUB, ]> { let cppNamespace = "::mlir::NPCOMP::Basicpy"; } //===----------------------------------------------------------------------===// // Comparison operation enum // The name matches the operation name in the python AST ("Lt", "Gt", etc). //===----------------------------------------------------------------------===// def CMPOP_EQ : StrEnumAttrCase<"Eq">; def CMPOP_GT : StrEnumAttrCase<"Gt">; def CMPOP_GTE : StrEnumAttrCase<"GtE">; def CMPOP_IN : StrEnumAttrCase<"In">; def CMPOP_IS : StrEnumAttrCase<"Is">; def CMPOP_ISNOT : StrEnumAttrCase<"IsNot">; def CMPOP_LT : StrEnumAttrCase<"Lt">; def CMPOP_LTE : StrEnumAttrCase<"LtE">; def CMPOP_NEQ : StrEnumAttrCase<"NotEq">; def CMPOP_NOTIN : StrEnumAttrCase<"NotIn">; def CompareOperationAttr : StrEnumAttr< "CompareOperation", "Comparison operator", [ CMPOP_EQ, CMPOP_GT, CMPOP_GTE, CMPOP_IN, CMPOP_IS, CMPOP_ISNOT, CMPOP_LT, CMPOP_LTE, CMPOP_NEQ, CMPOP_NOTIN, ]> { let cppNamespace = "::mlir::NPCOMP::Basicpy"; } //===----------------------------------------------------------------------===// // Constant and constructor operations //===----------------------------------------------------------------------===// def Basicpy_NumericConstantOp : Basicpy_Op<"numeric_constant", [ ConstantLike, NoSideEffect, DeclareOpInterfaceMethods]> { let summary = "A constant from the Python3 numeric type hierarchy"; let description = [{ Basicpy re-uses core MLIR types to represent the Python3 numeric type hierarchy with the following mappings: * Python3 `int` : In python, this type is signed, arbitrary precision but in typical realizations, it maps to an MLIR `IntegerType` of a fixed bit-width (typically si64 if no further information is known). In the future, there may be a real `Basicpy::IntType` that retains the true arbitrary precision nature, but this is deemed an enhancement that does not obviate the need to infer physical, sized types for many real-world cases. As such, the Basicpy numeric type hierarchy will always include physical `IntegerType`, if only to enable progressive lowering and interop with cases where the precise type is known. * Python3 `float` : This is allowed to map to any legal floating point type on the physical machine and is usually represented as a double (f64). In MLIR, any `FloatType` is allowed, which facilitates progressive lowering and interop with cases where a more precise type is known. * Python3 `complex` : Maps to an MLIR `ComplexType` with a `FloatType` elementType (note: in Python, complex numbers are always defined with floating point components). * `bool` : See `bool_constant` for a constant (i1) -> !basicpy.BoolType constant. This constant op is not used for representing such bool values, even though from the Python perspective, bool is part of the numeric hierarchy (the distinction is really only necessary during promotion). ### Integer Signedness All `int` values in Python are signed. However, there exist special cases where libraries (i.e. struct packing and numpy arrays) interoperate with unsigned values. As such, when mapping to MLIR, Python integer types are represented as either signed or unsigned `IntegerType` types and can be lowered to signless integers as appropriate (typically during realization of arithmetic expressions where the choice is meaningful). Since it is not known at the outset when in lowering this information is safe to discard this `numeric_constant` op accepts any signedness. }]; let arguments = (ins AnyAttr:$value); let results = (outs AnyType); let hasFolder = 1; } def Basicpy_BoolConstantOp : Basicpy_Op<"bool_constant", [ ConstantLike, NoSideEffect, DeclareOpInterfaceMethods]> { let summary = "A boolean constant"; let description = [{ A constant of type !basicpy.BoolType that can take either an i1 value of 0 (False) or 1 (True). Note that as in Python a BoolType can be thought of as an object, whereas the corresponding i1 is a numeric type suitable for use in contexts where storage format matters (or for interop with lower level dialects). }]; let arguments = (ins I1Attr:$value); let results = (outs Basicpy_BoolType:$result ); let assemblyFormat = "$value attr-dict"; let hasFolder = 1; } // TODO: Implement ConstantLike op trait. def Basicpy_BuildDictOp : Basicpy_Op<"build_dict", [NoSideEffect]> { let summary = "Builds an empty dict"; let description = [{ This op mirrors the CPython BUILD_MAP op (note naming difference). Note that as with CPython, this op only builds an empty dict; however, it is reserved in the future for it to take variadic operands to construct with a list of key/value pairs. }]; let arguments = (ins ); let results = (outs Basicpy_DictType:$result ); let assemblyFormat = "attr-dict `:` functional-type(operands, results)"; } // TODO: Implement ConstantLike op trait. def Basicpy_BuildListOp : Basicpy_Op<"build_list", [NoSideEffect]> { let summary = "Builds a list from operands"; let description = [{ Constructs a new list object from its operands. TODO: Any allowable type can be expressed in lists; however, this should be revisited once more of the dialect infrastructure is in place and tightened up accordingly. At that time, appropriate constraints should be added that both allow correct program representation and support transformations to lower levels (i.e. allowing a wider set of types as useful for conversions). }]; let arguments = (ins Variadic:$elements ); let results = (outs Basicpy_ListType:$result ); let assemblyFormat = "operands attr-dict `:` functional-type(operands, results)"; } // TODO: Implement ConstantLike op trait. def Basicpy_BuildTupleOp : Basicpy_Op<"build_tuple", [NoSideEffect]> { let summary = "Builds a tuple from operands"; let description = [{ Constructs a new tuple object from its operands. TODO: Any allowable type can be expressed in lists; however, this should be revisited once more of the dialect infrastructure is in place and tightened up accordingly. At that time, appropriate constraints should be added that both allow correct program representation and support transformations to lower levels (i.e. allowing a wider set of types as useful for conversions). }]; let arguments = (ins Variadic:$elements ); let results = (outs Basicpy_TupleType:$result ); let assemblyFormat = "operands attr-dict `:` functional-type(operands, results)"; } def Basicpy_BytesConstantOp : Basicpy_Op<"bytes_constant", [ ConstantLike, NoSideEffect, DeclareOpInterfaceMethods]> { let summary = "Constant bytes value"; let description = [{ A bytes value of BytesType. The value is represented by a StringAttr. }]; let arguments = (ins StrAttr:$value ); let results = (outs Basicpy_BytesType:$result ); let assemblyFormat = "$value attr-dict"; let hasFolder = 1; } def Basicpy_SingletonOp : Basicpy_Op<"singleton", [ ConstantLike, NoSideEffect]> { let summary = "Constant value for a singleton type"; let description = [{ Some types only have a single possible value, represented by the SingletonAttr. This op allows creating constants of these types. }]; let arguments = (ins); let results = (outs Basicpy_SingletonType:$result ); let assemblyFormat = "attr-dict `:` type($result)"; let hasFolder = 1; } def Basicpy_StrConstantOp : Basicpy_Op<"str_constant", [ ConstantLike, NoSideEffect, DeclareOpInterfaceMethods]> { let summary = "Constant string value"; let description = [{ A string value of StrType. The value is represented by a StringAttr that is UTF-8 encoded. }]; let arguments = (ins StrAttr:$value ); let results = (outs Basicpy_StrType:$result ); let assemblyFormat = "$value attr-dict"; let hasFolder = 1; } //===----------------------------------------------------------------------===// // Casting and coercion operations //===----------------------------------------------------------------------===// def Basicpy_AsI1Op : Basicpy_Op<"as_i1", [NoSideEffect]> { let summary = "Evaluates an input to an i1 predicate value"; let description = [{ Applies the rules for interpreting a type as a boolean, returning an i1 indicating the truthiness of the operand. Since the output of this op is intended to drive lower-level control flow, the i1 type is used (not the user level BoolType). }]; let arguments = (ins AnyType:$operand); let results = (outs I1:$result); let assemblyFormat = "$operand attr-dict `:` type($operand)"; } def Basicpy_BoolCastOp : Basicpy_Op<"bool_cast", [NoSideEffect]> { let summary = "Casts between BoolType and i1 (predicate value)"; let description = [{ When interfacing with lower level dialect or progressively lowering the Python BoolType away, it is often necessary to cast between it and i1, which is used to represent bool-ness at lower levels. }]; let arguments = (ins BoolOrI1Type:$operand); let results = (outs BoolOrI1Type:$result); let assemblyFormat = "$operand attr-dict `:` type(operands) `->` type(results)"; let hasFolder = 1; } def Basicpy_UnknownCastOp : Basicpy_Op<"unknown_cast", [NoSideEffect]> { let summary = "Casts to and from the UnknownType"; let arguments = (ins AnyType:$operand); let results = (outs AnyType:$result); let assemblyFormat = "operands attr-dict `:` type(operands) `->` type(results)"; let hasCanonicalizer = 1; } //===----------------------------------------------------------------------===// // Operations //===----------------------------------------------------------------------===// def Basicpy_BinaryCompareOp : Basicpy_Op<"binary_compare", []> { let summary = "Performs a comparison between two operands"; let description = [{ This op performs only one step of a potentially multi-step short circuit comparison. See: https://docs.python.org/3/reference/expressions.html#comparisons }]; let arguments = (ins AnyType:$left, AnyType:$right, CompareOperationAttr:$operation ); let results = (outs Basicpy_BoolType:$result ); let assemblyFormat = "$left $operation $right attr-dict `:` type(operands)"; } def Basicpy_BinaryExprOp : Basicpy_Op<"binary_expr", []> { let summary = "Binary expression"; let description = [{ An expression between two operands as generated by the AST BinOp node. }]; let arguments = (ins AnyType:$left, AnyType:$right, BinaryOperationAttr:$operation ); let results = (outs AnyType:$result ); let assemblyFormat = "$left $operation $right attr-dict `:` functional-type(operands, results)"; } def Basicpy_ExecOp : Basicpy_Op<"exec", [ SingleBlockImplicitTerminator<"ExecDiscardOp">]> { let summary = "Evaluates an expression being executed as a statement"; let description = [{ The result is discarded. Typically expressions are no-side-effect and can be re-ordered as needed. Embedding one in an exec op ensures that its placement in program order is preserved. }]; let regions = (region SizedRegion<1>:$body); let skipDefaultBuilders = 1; let builders = [ OpBuilder<(ins)>, ]; let extraClassDeclaration = [{ OpBuilder getBodyBuilder() { Block* body = getBody(0); return OpBuilder::atBlockEnd(body); } }]; } def Basicpy_ExecDiscardOp : Basicpy_Op<"exec_discard", [ NoSideEffect, ReturnLike, Terminator]> { let summary = "Terminator for an exec block"; let description = [{ Discards results and terminates an exec block. }]; let arguments = (ins Variadic:$operands); let assemblyFormat = "operands attr-dict `:` type(operands)"; } def Basicpy_FuncTemplateCallOp : Basicpy_Op<"func_template_call", []> { let summary = "Calls a function template"; let description = [{ Most function calls start with this generic calling op, which binds symbolically to a func_template. At this level, there are very few semantics associated with the call, since, often, both types and the specific concrete callee cannot be determined. Per python calling conventions, all functions return one result, even if None or a tuple (which may be syntactically unpacked to multiple results). If specified, the `argNames` operand is right aligned to the list of positional `args`, representing arguments that are special or have been passed with a keyword. The following arg names are special: '*': Indicates that the argument is a positional argument pack (must be the first arg name, if present). '**': Indicates that the argument is a keyword argument pack (must be the last arg name, if present). }]; let arguments = (ins FlatSymbolRefAttr:$callee, Variadic:$args, StrArrayAttr:$arg_names); let results = (outs AnyType:$result); let assemblyFormat = "$callee `(` $args `)` `kw` $arg_names attr-dict `:` functional-type($args, results)"; let skipDefaultBuilders = 1; let builders = [ OpBuilder<(ins)>, ]; } def Basicpy_FuncTemplateOp : Basicpy_Op<"func_template", [ IsolatedFromAbove, SingleBlockImplicitTerminator<"FuncTemplateTerminatorOp">, NativeOpTrait<"SymbolTable">, Symbol]> { let summary = "Group of multiple overload-resolved concrete functions"; let description = [{ The outer func_template op acts as a module that can contain named concrete functions that are interpreted as overloads. If the function signature is sufficient to disambiguate (i.e. with nothing more than arity and MLIR argument types), then this is all that is needed. However, in many cases, additional attributes will need to be specified to further constrain types. The first matching function signature is selected to satisfy a `func_template_call` op. TODO: Define this extended constraint matching. Instantiation ------------- Once a concrete function is selected as being applicable to a given call, it will typically be instantiated as a standalone, unspecialized function in the calling module (as a peer to the func_template). This function will be uniquely identified by concating the outer func_template's symbol name, '$', and the concrete instance's symbol name. Note that the function may still be unspecialized (in that it contains UnknownType arguments/results), and type inference is expected to further specialize/inline/constrain it. Naming ------ By convention, func_templates are named to avoid collision for various uses: - Global function templates: "__global$python.qualified.name" - Method names: "__method$method_name" - Attribute getter: "__getattr$attr_name" - Attribute setter: "__setattr$attr_name" As in user-level python, for functions that bind to an instance, the first argument must be a concrete type for the bound instance type. In this way, there is one `func_template` for every unique member name and the normal type constraints system is used to select the overload, just as if it was a normal function call. It is left to utility routines to merge libraries in a way that preserves this invariant. TODO: This needs to be fleshed out more as some additional rules about ordering and conflict resolution are likely needed to make this correct. Correlation with python runtime ------------------------------- When extracting a program, it is typically necessary to create weak references to specific python functions and correlate them back to a named template defined here. Often times this can just be done lexically, but to avoid fragility, any func_template that correlates to a python runtime function will have an additional attribute `py_bind` that is an array of StringAttr qualified names to resolve and bind to in the python runtime. In cases of divergence, the symbol name of the template should be chosen just for uniqueness (not significance). The qualified name format for `py_bind` attribute is: package.name#local.qualified.name }]; let arguments = (ins); let regions = (region SizedRegion<1>:$body); let skipDefaultBuilders = 1; let builders = [ OpBuilder<(ins)>, ]; let extraClassDeclaration = [{ OpBuilder getBodyBuilder() { Block* body = getBody(0); return OpBuilder::atBlockEnd(body); } }]; } def Basicpy_FuncTemplateTerminatorOp : Basicpy_Op<"func_template_terminator", [ HasParent<"Basicpy::FuncTemplateOp">, Terminator]> { let summary = "Terminator pseudo-op for the FuncTemplateOp"; let parser = ?; let printer = ?; } def Basicpy_SlotObjectMakeOp : Basicpy_Op<"slot_object_make", [ NoSideEffect]> { let summary = "Creates an instance of a SlotObject type"; let description = [{ SlotObjects are typically instances of built-in classes that have a fixed number of slots. Unlike in standard python, the types of each slot are tracked. This op has a custom assembly form which can be used when valid that omits the operand types (since they are equal to the types in the returned slot object). Example: %0 = basicpy.singleton : !basicpy.NoneType %1 = basicpy.slot_object_make(%0) -> !basicpy.SlotObject }]; let arguments = (ins // TODO: Tighter constraints on allowable types. Variadic:$slots ); let results = (outs Basicpy_SlotObjectType:$result ); } def Basicpy_SlotObjectGetOp : Basicpy_Op<"slot_object_get", [ NoSideEffect]> { let summary = "Gets a slot from a slot object"; let description = [{ Gets a slot from a SlotObject. Example: %0 = basicpy.slot_object_make ... %1 = basicpy.slot_object_get %0[1] : !basicpy.SlotObject<...> }]; let arguments = (ins Basicpy_SlotObjectType:$object, IndexAttr:$index ); let results = (outs AnyType:$result ); } #endif // NPCOMP_DIALECT_BASICPY_IR_BASICPY_OPS