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).