OK @pschultz, I’m convinced EmitTriplet
should be a COption[PValue]
. Concretely:
trait EmitTriplet {
def apply(missing: () => Code[Nothing], present: (PValue) => Code[Nothing]): Code[Nothing]
}
although I would prefer to write:
case class EmitTriplet(f: (() => Code[Nothing], (PValue) => Code[Nothing]) => Code[Nothing])
to avoid the syntactic overhead of the method declaration and write:
EmitTriplet { (missing, present) => ... }
The motivation for this I think is most clearly seen in the code generation rule for ArrayRef
. Currently:
val setup = Code(
codeA.setup,
xma := codeA.m,
xa := pArray.defaultValue,
codeI.setup,
xmi := codeI.m,
xi := coerce[Int](defaultValue(TInt32())),
len := coerce[Int](defaultValue(TInt32())),
(xmi || xma).mux(
xmv := const(true),
Code(
xa := codeA.pv,
xi := coerce[Int](codeI.v),
len := pArray.loadLength(xa.load().tcode[Long]),
(xi < len && xi >= 0).mux(
xmv := !pArray.isElementDefined(xa.load().tcode[Long], xi),
Code._fatal(errorTransformer(
const("array index out of bounds: index=")
.concat(xi.load().toS)
.concat(", length=")
.concat(len.load().toS)))))))
EmitTriplet(setup, xmv, PValue(pt,
Region.loadIRIntermediate(x.pType)(pArray.elementOffset(xa.load().tcode[Long], len, xi))))
This computes m up front and stores it. Clearly that code is horrendous. It should be three ifs (with two jumps to the common false case) and then the code that uses the element’s value. With this proposal, the code would look like:
val L = new CodeLabel()
EmitTriplet { (missing, present) =>
a(Code(L, missing), { av =>
i(L.goto, { iv =>
Code(
xa := av,
xi := iv,
len := pArray.loadLength(xa.load().tcode[Long]),
(xi < len && xi >= 0).mux(
pArray.isElementDefined(xa.load().tcode[Long], xi)
.mux(present(
PValue(pt, Region.loadIRIntermediate(x.pType)(pArray.elementOffset(xa.load().tcode[Long], len, xi)))),
L.goto),
Code._fatal(errorTransformer(
const("array index out of bounds: index=")
.concat(xi.load().toS)
.concat(", length=")
.concat(len.load().toS))))))))
}
This could be done incrementally, although switching from the labels to the values means introducing a local temporary which currently requires a MethodBuilder. With some abuse of notation:
class EmitTriplet {
def apply(setup: Code[Unit], m: Code[Boolean], pv: PValue): EmitTriplet =
new EmitTriplet {
def emit(missing: Code[Unit], present: (PValue) => Code[Nothing]): Code[Nothing] = {
m.mux(missing, present(pv))
}
def toValues(mb: MethodBuilder, et: EmitTriplet): EmitTriplet = {
val L = new CodeLabel()
val x = mb.newPField(et.pt)
Code(et(m := true, L.goto), v2 => Code(x := false, v := v2, L.goto), L)
}
}
However, lir can support local creation without a method builder. So maybe it makes sense to attempt it incrementally after that goes in. Also, there aren’t a ton of uses of it, so someone might be able to do it in one go.
I want to discuss what this looks like in the universe where Code has been split into (imperative) CodeBuilder and a value-carrying (non-Unit) Value[T]. This is what I called it in the other dev post, it is different than the Value[T] I just PR’ed.
A function returning Code[T] in the old world corresponds to a function taking a CodeBuilder and (possibly) returning a Value[T]. So the EmitTriplet becomes:
trait EmitTriplet {
def apply(cb: CodeBuilder,
missing: (CodeBuilder) => Unit,
present: (CodeBuilder, PValue) => Unit): Unit
}
And what are we going to call this thing?
I assume @pschultz is on board with this. Comments, anyone else?