github.com/ipld/go-ipld-prime@v0.21.0/node/basicnode/HACKME.md (about) 1 hackme 2 ====== 3 4 Design rationale are documented here. 5 6 This doc is not necessary reading for users of this package, 7 but if you're considering submitting patches -- or just trying to understand 8 why it was written this way, and check for reasoning that might be dated -- 9 then it might be useful reading. 10 11 ### scalars are just typedefs 12 13 This is noteworthy because in codegen, this is typically *not* the case: 14 in codegen, even scalar types are boxed in a struct, such that it prevents 15 casting values into those types. 16 17 This casting is not a concern for the node implementations in this package, because 18 19 - A) we don't have any kind of validation rules to make such casting worrying; and 20 - B) since our types are unexported, casting is still blocked by this anyway. 21 22 ### about builders for scalars 23 24 The assembler types for scalars (string, int, etc) are pretty funny-looking. 25 You might wish to make them work without any state at all! 26 27 The reason this doesn't fly is that we have to keep the "wip" value in hand 28 just long enough to return it from the `NodeBuilder.Build` method -- the 29 `NodeAssembler` contract for `Assign*` methods doesn't permit just returning 30 their results immediately. 31 32 (Another possible reason is if we expected to use these assemblers on 33 slab-style allocations (say, `[]plainString`)... 34 however, this is inapplicable at present, because 35 A) we don't (except places that have special-case internal paths anyway); and 36 B) the types aren't exported, so users can't either.) 37 38 Does this mean that using `NodeBuilder` for scalars has a completely 39 unnecessary second allocation, which is laughably inefficient? Yes. 40 It's unfortunate the interfaces constrain us to this. 41 **But**: one typically doesn't actually use builders for scalars much; 42 they're just here for completeness. 43 So this is less of a problem in practice than it might at first seem. 44 45 More often, one will use the "any" builder (which is has a whole different set 46 of design constraints and tradeoffs); 47 or, if one is writing code and knows which scalar they need, the exported 48 direct constructor function for that kind 49 (e.g., `String("foo")` instead of `Prototype__String{}.NewBuilder().AssignString("foo")`) 50 will do the right thing and do it in one allocation (and it's less to type, too). 51 52 ### maps and list keyAssembler and valueAssemblers have custom scalar handling 53 54 Related to the above heading. 55 56 Maps and lists in this package do their own internal handling of scalars, 57 using unexported features inside the package, because they can more efficient. 58 59 ### when to invalidate the 'w' pointers 60 61 The 'w' pointer -- short for 'wip' node pointer -- has an interesting lifecycle. 62 63 In a NodeAssembler, the 'w' pointer should be intialized before the assembler is used. 64 This means either the matching NodeBuilder type does so; or, 65 if we're inside recursive structure, the parent assembler did so. 66 67 The 'w' pointer is used throughout the life of the assembler. 68 69 Setting the 'w' pointer to nil is one of two mechanisms used internally 70 to mark that assembly has become "finished" (the other mechanism is using 71 an internal state enum field). 72 Setting the 'w' pointer to nil has two advantages: 73 one is that it makes it *impossible* to continue to mutate the target node; 74 the other is that we need no *additional* memory to track this state change. 75 However, we can't use the strategy of nilling 'w' in all cases: in particular, 76 when in the NodeBuilder at the root of some construction, 77 we need to continue to hold onto the node between when it becomes "finished" 78 and when Build is called; otherwise we can't actually return the value! 79 Different stratgies are therefore used in different parts of this package. 80 81 Maps and lists use an internal state enum, because they already have one, 82 and so they might as well; there's no additional cost to this. 83 Since they can use this state to guard against additional mutations after "finish", 84 the map and list assemblers don't bother to nil their own 'w' at all. 85 86 During recursion to assemble values _inside_ maps and lists, it's interesting: 87 the child assembler wrapper type takes reponsibility for nilling out 88 the 'w' pointer in the child assembler's state, doing this at the same time as 89 it updates the parent's state machine to clear proceeding with the next entry. 90 91 In the case of scalars at the root of a build, we took a shortcut: 92 we actually don't fence against repeat mutations at all. 93 *You can actually use the assign method more than once*. 94 We can do this without breaking safety contracts because the scalars 95 all have a pass-by-value phase somewhere in their lifecycle 96 (calling `nb.AssignString("x")`, then `n := nb.Build()`, then `nb.AssignString("y")` 97 won't error if `nb` is a freestanding builder for strings... but it also 98 won't result in mutating `n` to contain `"y"`, so overall, it's safe). 99 100 We could normalize the case with scalars at the root of a tree so that they 101 error more aggressively... but currently we haven't bothered, since this would 102 require adding another piece of memory to the scalar builders; and meanwhile 103 we're not in trouble on compositional correctness. 104 105 Note that these remarks are for the `basicnode` package, but may also 106 apply to other implementations too (e.g., our codegen output follows similar 107 overall logic). 108 109 ### NodePrototypes are available through a singleton 110 111 Every NodePrototype available from this package is exposed as a field 112 in a struct of which there's one public exported instance available, 113 called 'Prototype'. 114 115 This means you can use it like this: 116 117 ```go 118 nbm := basicnode.Prototype.Map.NewBuilder() 119 nbs := basicnode.Prototype.String.NewBuilder() 120 nba := basicnode.Prototype.Any.NewBuilder() 121 // etc 122 ``` 123 124 (If you're interested in the performance of this: it's free! 125 Methods called at the end of the chain are inlinable. 126 Since all of the types of the structures on the way there are zero-member 127 structs, the compiler can effectively treat them as constants, 128 and thus freely elide any memory dereferences that would 129 otherwise be necessary to get methods on such a value.) 130 131 ### NodePrototypes are (also) available as exported concrete types 132 133 The 'Prototype' singleton is one way to access the NodePrototype in this package; 134 their exported types are another equivalent way. 135 136 ```go 137 basicnode.Prototype.Map = basicnode.Prototype.Map 138 ``` 139 140 It is recommended to use the singleton style; 141 they compile to identical assembly, and the singleton is syntactically prettier. 142 143 We may make these concrete types unexported in the future. 144 A decision on this is deferred until some time has passed and 145 we can accumulate reasonable certainty that there's no need for an exported type 146 (such as type assertions, etc).