Commit 9de3d223 authored by Eric Myhre's avatar Eric Myhre

Finish AssignNode handling for recursives.

Update to the hackme document to match.
(Turns out a decent amount of early speculations about how handy it
would be to nil out the 'w' pointer ended up a bit misguided.)

This spawns another one of those things that's dang hard to test:
half of these functions are for handling the case that something of
the right *kind* but a different *style*/implementation is given...
which we can't test except by ramming two different node
implementations together.  Even later, that's going to make things
rather Interesting, because the test package dependencies will
include one concrete node package and then be that much less usable
by that package itself (ugh); getting correct coverage attribution
to the odd package out there will be hard; and for now, it means we
just plain have no tests for those branches.  Making tests to check
compatibility and correctness of these branches once we get more
codegen integration up will be fun, though.
parent b3c369ae
......@@ -65,25 +65,41 @@ if we're inside recursive structure, the parent assembler did so.
The 'w' pointer is used throughout the life of the assembler.
When assembly becomes "finished", the 'w' pointer should be set to nil,
in order to make it impossible to continue to mutate that node.
However, this doesn't *quite* work... because in the case of builders (at the root),
Setting the 'w' pointer to nil is one of two mechanisms used internally
to mark that assembly has become "finished" (the other mechanism is using
an internal state enum field).
Setting the 'w' pointer to nil has two advantages:
one is that it makes it *impossible* to continue to mutate the target node;
the other is that we need no *additional* memory to track this state change.
However, we can't use the strategy of nilling 'w' in all cases: in particular,
when in the NodeBuilder at the root of some construction,
we need to continue to hold onto the node between when it becomes "finished"
and when Build is called, allowing us to return it (and then finally nil 'w').
This has some kinda wild implications. In recursive structures, it means the
child assembler wrapper type is the one who takes reponsibility for nilling out
the 'w' pointer at the same time as it updates the parent's state machine to
proceed with the next entry. In the case of scalars at the root of a build,
it means *you can actually use the assign method more than once*.
We could normalize the case with scalars at the root of a tree by adding another
piece of memory to the scalar builders... but currently we haven't bothered.
We're not in trouble on compositional correctness because nothing's visible
until Build is called (whereas recursives have to be a lot stricter around this,
because if something gets validated and finished, it's already essential that
it now be unable to mutate out of the validated state, even if it's also not
yet 'visible').
and when Build is called; otherwise we can't actually return the value!
Different stratgies are therefore used in different parts of this package.
Maps and lists use an internal state enum, because they already have one,
and so they might as well; there's no additional cost to this.
Since they can use this state to guard against additional mutations after "finish",
the map and list assemblers don't bother to nil their own 'w' at all.
During recursion to assemble values _inside_ maps and lists, it's interesting:
the child assembler wrapper type takes reponsibility for nilling out
the 'w' pointer in the child assembler's state, doing this at the same time as
it updates the parent's state machine to clear proceeding with the next entry.
In the case of scalars at the root of a build, we took a shortcut:
we actually don't fence against repeat mutations at all.
*You can actually use the assign method more than once*.
We can do this without breaking safety contracts because the scalars
all have a pass-by-value phase somewhere in their lifecycle
(calling `nb.AssignString("x")`, then `n := nb.Build()`, then `nb.AssignString("y")`
won't error if `nb` is a freestanding builder for strings... but it also
won't result in mutating `n` to contain `"y"`, so overall, it's safe).
We could normalize the case with scalars at the root of a tree so that they
error more aggressively... but currently we haven't bothered, since this would
require adding another piece of memory to the scalar builders; and meanwhile
we're not in trouble on compositional correctness.
Note that these remarks are for the `basicnode` package, but may also
apply to other implementations too (e.g., our codegen output follows similar
......
......@@ -181,11 +181,36 @@ func (plainList__Assembler) AssignLink(ipld.Link) error {
return mixins.ListAssembler{"list"}.AssignLink(nil)
}
func (na *plainList__Assembler) AssignNode(v ipld.Node) error {
// todo: apply a generic 'copy' function.
// todo: probably can also shortcut to copying na.t and na.m if it's our same concrete type?
// (can't quite just `na.w = v`, because we don't have 'freeze' features, and we don't wanna open door to mutation of 'v'.)
// (wait... actually, probably we can? 'Assign' is a "finish" method. we can&should invalidate the wip pointer here.)
panic("later")
// Sanity check, then update, assembler state.
if na.state != laState_initial {
panic("misuse")
}
na.state = laState_finished
// Copy the content.
if v2, ok := v.(*plainList); ok { // if our own type: shortcut.
// Copy the structure by value.
// This means we'll have pointers into the same internal maps and slices;
// this is okay, because the Node type promises it's immutable, and we are going to instantly finish ourselves to also maintain that.
*na.w = *v2
return nil
}
// If the above shortcut didn't work, resort to a generic copy.
// We call AssignNode for all the child values, giving them a chance to hit shortcuts even if we didn't.
if v.ReprKind() != ipld.ReprKind_List {
return ipld.ErrWrongKind{TypeName: "list", MethodName: "AssignNode", AppropriateKind: ipld.ReprKindSet_JustList, ActualKind: v.ReprKind()}
}
itr := v.ListIterator()
for !itr.Done() {
_, v, err := itr.Next()
if err != nil {
return err
}
if err := na.AssembleValue().AssignNode(v); err != nil {
return err
}
}
// validators could run and report errors promptly, if this type had any -- same as for regular Finish.
return nil
}
func (plainList__Assembler) Style() ipld.NodeStyle {
return Style__List{}
......
......@@ -197,11 +197,39 @@ func (plainMap__Assembler) AssignLink(ipld.Link) error {
return mixins.MapAssembler{"map"}.AssignLink(nil)
}
func (na *plainMap__Assembler) AssignNode(v ipld.Node) error {
// todo: apply a generic 'copy' function.
// todo: probably can also shortcut to copying na.t and na.m if it's our same concrete type?
// (can't quite just `na.w = v`, because we don't have 'freeze' features, and we don't wanna open door to mutation of 'v'.)
// (wait... actually, probably we can? 'Assign' is a "finish" method. we can&should invalidate the wip pointer here.)
panic("later")
// Sanity check, then update, assembler state.
if na.state != maState_initial {
panic("misuse")
}
na.state = maState_finished
// Copy the content.
if v2, ok := v.(*plainMap); ok { // if our own type: shortcut.
// Copy the structure by value.
// This means we'll have pointers into the same internal maps and slices;
// this is okay, because the Node type promises it's immutable, and we are going to instantly finish ourselves to also maintain that.
*na.w = *v2
return nil
}
// If the above shortcut didn't work, resort to a generic copy.
// We call AssignNode for all the child values, giving them a chance to hit shortcuts even if we didn't.
if v.ReprKind() != ipld.ReprKind_Map {
return ipld.ErrWrongKind{TypeName: "map", MethodName: "AssignNode", AppropriateKind: ipld.ReprKindSet_JustMap, ActualKind: v.ReprKind()}
}
itr := v.MapIterator()
for !itr.Done() {
k, v, err := itr.Next()
if err != nil {
return err
}
if err := na.AssembleKey().AssignNode(k); err != nil {
return err
}
if err := na.AssembleValue().AssignNode(v); err != nil {
return err
}
}
// validators could run and report errors promptly, if this type had any -- same as for regular Finish.
return nil
}
func (plainMap__Assembler) Style() ipld.NodeStyle {
return Style__Map{}
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment