forked from llvm/torch-mlir
-
Notifications
You must be signed in to change notification settings - Fork 0
/
BasicpyOps.td
540 lines (481 loc) · 20.1 KB
/
BasicpyOps.td
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
//===- 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<OpAsmOpInterface>]> {
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<OpAsmOpInterface>]> {
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<AnyType>:$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<AnyType>:$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<OpAsmOpInterface>]> {
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<OpAsmOpInterface>]> {
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)";
}
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<AnyType>:$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<AnyType>:$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<slice, !basicpy.NoneType>
}];
let arguments = (ins
// TODO: Tighter constraints on allowable types.
Variadic<AnyType>:$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