Is there a generic way to hack discriminant assignment in a way that works for both VM and C backend (and Js backend as well if possible). I've come up with a solution that works for a C backend, but it fails in a VM.
type
Obj = object
notKind: bool
case kind: bool
of true: discard
of false: discard
template setKind*[T, K](t: var T, kind: untyped, target: K) =
cast[ptr K](cast[int](addr t) + offsetOf(typeof(t), kind))[] = target
var test = Obj()
echo test
setKind test, kind, true
echo test
(notKind: false, kind: false)
(notKind: false, kind: true)
I need this to fix another issue -
type
Requires {.requiresinit.} = object
# Object MUST be a requiresinit for a reasons that are out of the scope of this
# question (and the code I have to write now as well). It is a given requirement.
Obj = object
case kind: bool
of true:
req: Requires
of false: discard
func build(inKind: bool): Obj =
# I need to init `Obj` with a runtime value for a discriminant, but it is not possible
# because
Obj(kind: inKind, req: default(Requires)) # Error: The Obj type requires the following fields to be initialized: req.
Obj(kind: inKind, req: default(Requires)) # Error: cannot prove that it's safe to initialize 'req' with the runtime value for the discriminator 'kind'
# I'm cornered by 'requires init' and 'cannot change discriminant', so I need hack around this
# limitation using
setKind(result, kind, inKind)
if result.kind == true:
result.req = default(Requires)
# Which is possible on a C backend, not on other ones.
This artificial limitation, paired with compile-time check kind field initialization (ProveInit) significantly increases complexity of code generation logic for (de)serialization implemenentation, specifically for cases with nested case fields. Code that otherwise could be written using functions that directly load discriminant fields
{.cast(safeAssign).}:
var obj = Variant()
load(obj.toplevelKind)
case obj.toplevelKind:
of opt1, opt2: # Can support multiple discriminant values
load(obj.nestedKind)
must currently be written using very verbose pattern
var kind1: KindField
load(kind1)
var obj: Obj()
case kind1:
of opt1:
var kind2: NestedKind
load(kind2)
obj = Obj(kind1: opt1, nestedKind: kind2)
of opt2:
var kind2: NestedKind
load(kind2)
obj = Obj(kind1: opt2, nestedKind: kind2)
I fixed this particular issue by wrapping {.requiresinit.} type in seq[Requires]. Prover was happy this this solution, and it is roughly the same as my original idea, but even worse.
setKind(result, kind, inKind)
if result.kind == true:
result.req = default(Requires)
This is not entirely unrelated, but I things there must be a good built-in way to disable this checking locally, because sometimes it really messes up things (specifically serialization and initialization with cases like these).
About #368 - I'm not even sure it is possible to implement prover that would guarantee assignment safety in the simple (current) case. It would need some sort of typestate/dataflow analysis to figure out set of all possible initialization values, and act based on that. So, I suppose I'm missing something here, but we are already in trouble with single discriminant (and we can have multiple discriminant fields at the same time as well), so how having shared fields would make tings harder?
type
NodeKind = enum
foo
bar
bif
baz
bad
Node = object
case kind: NodeKind
of foo, bar, baz:
children: seq[Node]
of bad, bif:
dad: Node
case kind:
of foo:
additionalFieldForFoo: int
of bar:
onlyValidForBar: float
else:
discard
case kind:
of foo, bar:
validForFooAndBar: string
else:
discard
proc node(kind: NodeKind) =
case kind:
of foo:
# safe to init `children`, `additionalFieldForFoo`, `validForFooAndBar`
# Assigning to any other field would be a compile-time error
# with 'cannot init'. With `{.cast(safeAssign).}` it would turn into
# a warning? hint? nothing?
of bar:
# safe to init `children`, `onlyValidForBar`, `validForFooAndBar`
of bif:
# safe to init `dad`
of baz:
# safe to init `children`
of bad:
# safe to init `dad`
# When trying to init `additionalFieldForFoo` shoudl error out (as it does now),
# with something like 'cannot prove <XXX> init safety, with unchecked runtime value
# for discriminant - possible values are <list of dataflow-inferred values>
This is not a proposal of any sort, but for me it doesn't look like things are going to be worse. At least that's my current understanding.
I'd use
proc `kind=`(var self: Obj, kind: bool) =
res = Obj(kind: kind)
res.notKind = self.notKind
self = res