Hi Gil, Thank you for the detailed RFC.
I thought about a variation of your second suggestion, that may work without having to work with symbols all the time. We could make lvalues part of the type system by introducing an !emitc.lvalue
type. The emitc.automatic
operation or a repurposed emitc.variable
would produce a value which is wrapped in this type. The emitc.address_of
op would be restricted to this type as well as the destination operand of the emitc.assign
op. To make the usage of the value explicit in the IR I could imagine an emitc.lvalue_to_rvalue
operation (similar to your emitc.read
on symbols I guess) that represents lvalue conversion with a read memory effect.
Either way I am not quite sure how this would be handled in the emitter. Should the emitter skip the op and print the lvalue/symbol instead for every use? This would mean that the memory read is posponed to the uses in the generated code leading to a semantic mismatch between the IR and the generated code. Making all EmitC ops lvalue agnostic may be an option, we’d need to have a least custom func, call and return ops for this.
Example:
func.func @lvalue_variables(%v1: i32, %v2: i32) -> i32 {
%val = emitc.mul %v1, %v2 : (i32, i32) -> i32
%variable = emitc.variable : !emitc.lvalue<i32> // alloc effect
emitc.assign %val : i32 to %variable : !emitc.lvalue<i32> // write effect
%addr = emitc.address_of %variable : !emitc.lvalue<i32> -> !emitc.ptr<i32>
emitc.call_opaque "zero" (%addr) : (i32) -> ()
%updated_val = emitc.lvalue_to_rvalue %variable : i32 // read effect, (noop in emitter?)
%neg_one = arith.constant -1 : i32
emitc.assign %neg_one : i32 to %variable : !emitc.lvalue<i32> // invalidates %updated_val
return %updated_val : i32
// dealloc effect through automatic allocation scope
}
Simon