hackme ====== Design rationale are documented here. This doc is not necessary reading for users of this package, but if you're considering submitting patches -- or just trying to understand why it was written this way, and check for reasoning that might be dated -- then it might be useful reading. ### scalars are just typedefs This is noteworthy because in codegen, this is typically *not* the case: in codegen, even scalar types are boxed in a struct, such that it prevents casting values into those types. This casting is not a concern for the node implementations in this package, because - A) we don't have any kind of validation rules to make such casting worrying; and - B) since our types are unexported, casting is still blocked by this anyway. ### about builders for scalars The assembler types for scalars (string, int, etc) are pretty funny-looking. You might wish to make them work without any state at all! The reason this doesn't fly is that we have to keep the "wip" value in hand just long enough to return it from the `NodeBuilder.Build` method -- the `NodeAssembler` contract for `Assign*` methods doesn't permit just returning their results immediately. (Another possible reason is if we expected to use these assemblers on slab-style allocations (say, `[]plainString`)... however, this is inapplicable at present, because A) we don't (except places that have special-case internal paths anyway); and B) the types aren't exported, so users can't either.) Does this mean that using `NodeBuilder` for scalars has a completely unnecessary second allocation, which is laughably inefficient? Yes. It's unfortunate the interfaces constrain us to this. **But**: one typically doesn't actually use builders for scalars much; they're just here for completeness. So this is less of a problem in practice than it might at first seem. More often, one will use the "any" builder (which is has a whole different set of design constraints and tradeoffs); or, if one is writing code and knows which scalar they need, the exported direct constructor function for that kind (e.g., `String("foo")` instead of `Prototype__String{}.NewBuilder().AssignString("foo")`) will do the right thing and do it in one allocation (and it's less to type, too). ### maps and list keyAssembler and valueAssemblers have custom scalar handling Related to the above heading. Maps and lists in this package do their own internal handling of scalars, using unexported features inside the package, because they can more efficient. ### when to invalidate the 'w' pointers The 'w' pointer -- short for 'wip' node pointer -- has an interesting lifecycle. In a NodeAssembler, the 'w' pointer should be intialized before the assembler is used. This means either the matching NodeBuilder type does so; or, if we're inside recursive structure, the parent assembler did so. The 'w' pointer is used throughout the life of the assembler. 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; 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 overall logic). ### NodePrototypes are available through a singleton Every NodePrototype available from this package is exposed as a field in a struct of which there's one public exported instance available, called 'Prototype'. This means you can use it like this: ```go nbm := basicnode.Prototype.Map.NewBuilder() nbs := basicnode.Prototype.String.NewBuilder() nba := basicnode.Prototype.Any.NewBuilder() // etc ``` (If you're interested in the performance of this: it's free! Methods called at the end of the chain are inlinable. Since all of the types of the structures on the way there are zero-member structs, the compiler can effectively treat them as constants, and thus freely elide any memory dereferences that would otherwise be necessary to get methods on such a value.) ### NodePrototypes are (also) available as exported concrete types The 'Prototype' singleton is one way to access the NodePrototype in this package; their exported types are another equivalent way. ```go basicnode.Prototype.Map = basicnode.Prototype.Map ``` It is recommended to use the singleton style; they compile to identical assembly, and the singleton is syntactically prettier. We may make these concrete types unexported in the future. A decision on this is deferred until some time has passed and we can accumulate reasonable certainty that there's no need for an exported type (such as type assertions, etc).