A question on type inference for operands

Hello everyone. To be completely honest, I am not sure why operands have to somehow specify their type directly (with type-directive) or indirectly (with type inference interfaces) for the type inference to work.

Isn’t it possible to refer to the operation the value was created with in order to omit rather needless repetition, when you could have only specified the types of each result?

I understand that I might be missing some kind of a workaround for the problem or its general intrinsics, so I will be quite grateful if you can help me. Thank you in advance.

Are you taking about the textual format, the building API, of the InferTypeOpInterface mechanisms?

Maybe you could provide some examples?

I am talking about the textual assembly format. Here is one suitable example:

let’s define a constant-creating operation const_op, which returns a value of a type, satisfying a constraint const (either with a type constI or constF).

def ConstInt : DFCIR_DialectType<"DFCIRConstInt", "constI"> {
	let parameters = (ins Builtin_IntegerAttr:$value);
	let assemblyFormat = "`<` $value `>`";
}

def ConstFloat : DFCIR_DialectType<"DFCIRConstFloat", "constF"> {
	let parameters = (ins Builtin_FloatAttr:$value);
	let assemblyFormat = "`<` $value `>`";
}

def Const : TypeConstraint<Or<[ConstInt.predicate, ConstFloat.predicate]>, "const">;

def ConstOp : DFCIR_Op<"const_op"> {
	let assemblyFormat = " type($res) attr-dict";
	
	let results = (outs Const:$res);
}

So in .mlir-file it will look something like this:

%zeroConst = dfcir.const_op dfcir.constI<0>
%oneConst = dfcir.const_op dfcir.constI<1>
%twoConst = dfcir.const_op dfcir.constI<2>
%threeConst = dfcir.const_op dfcir.constI<3>

Then, let’s say we have a logical operation less, which can have operands of types, satisfying the constraint variable, where not only const types apply, but stream and scalar types as well:

def Stream : DFCIR_DialectType<"DFCIRStream", "stream"> {
	let parameters = (ins "Type":$streamType);
	let assemblyFormat = "`<` $streamType `>`";
}

def Scalar : DFCIR_DialectType<"DFCIRScalar", "scalar"> {
	let parameters = (ins "Type":$scalarType);
	let assemblyFormat = "`<` $scalarType `>`";
}

def Variable : TypeConstraint<Or<[Stream.predicate, Scalar.predicate, Const.predicate]>, "variable">;

def LessOp : DFCIR_Op<"less"> {
	let arguments = (ins
		Variable:$first,
		Variable:$second);
		
	let assemblyFormat = "`(` $first `:` type($first) `,` $second `:` type($second)`)` attr-dict `:` type($res)";
	
	let results = (outs Variable:$res);
}

Because all the operands can have different types (we cannot use traits like SameOperandsAndResultType) as well as the operation’s result, the assembly format to compare two literals - %intValue and %floatValue and return a literal of boolean type will look like this:

%intValue = dfcir.const_op dfcir.constI<0>
%floatValue= dfcir.const_op dfcir.constF<2.5>

%result = dfcir.less(%intValue : dfcir.constI<0>, %floatValue : dfcir.constF<2.5>) : dfcir.constI<1>

I understand that in this case it is correct to somehow specify the type of resulting value, but in case with the operands - they all have “parent operations”, where their types were already specified, so it seems strange to me to specify the same types again, when the same type specifications can be extracted from the parent operation.

Once again, I am sorry if questions like this might seem weird to you, but I honestly have little experience with MLIR and have little understanding of its intrinsics.

Nothing weird about the question, it was just ambiguous in terms of which part of the system you were referring to.

You can hand-write C++ parsing that operates with this, the declarative assembly does not allow it because we consider this against good practices: the principle is that each operation should be able to be parsed in isolation and all the information necessary to parse it should be self-contained in the format.

It originally comes from the fact that for composability reasons, you can never know that there won’t be a CFG enclosing some IR at some point of the lowering, and in a CFG you need to parse the users before the definition in some cases: so you just don’t have yet seen the type of the operands.

For the result, you can provide “type inference” by implementing the InferTypeOpInterface for the operation, if you do so then the type of the result is optional in the textual assembly since it can be inferred from the type of the operands.

1 Like

Thank you very much for the explanation. I think I got it right.