Copy-on-write was harder than I thought it’d be.
My first implementation was just going to CoW raw string data, and just duplicate the actual yapValue structures over and over, which (in retrospect) was dumb. The yapValue structure had two bools on it: “used” and “shared”, which was a very verbose way of marking a value up to twice (shared is marked if you try to mark a used value). I figured if I cleared all used/shared bools and did a marking pass, that I could safely do whatever I wanted to anything not marked as shared. This is bad.
I can’t assume that I will be doing a garbage collection pass on every op. I’m not sure what the “recommended” op count per GC will be, but doing it for each op is incredibly slow, and the only way the “shared” bool would ever be useful. Also, knowing if YOU are the reason a yapValue is flagged as “used” is impossible in certain contexts, so deciding to continue to manipulate a non-shared object would have been a big mistake.
It turns out the answer was in yapValue’s exposed interface. I changed all of the yapValueSet*() calls to return a yapValue* (instead of void), and added a new function which each calls as the function begins, like this:
yapValue * yapValueSetInt(struct yapVM *vm, yapValue *p, int v)
p = yapValuePersonalize(vm, p);
I needed to come up with a word that meant “clone the value if it is used”, and kept picturing words like “mine”, “solo”, “privatize”, and ultimately “personalize”. The trick here is that a fresh yapValue structure will not have the used bool set on it, so allocating one and immediately setting its value has the obvious effect. However, once a yapValue is set, it is set in stone until it is released (and returned back to the yapValue free list). I removed the shared bool and audited my code for any bad usages of yapValue. I had to add a couple more yapValueSet*() functions for completeness, but it worked on the first try, if you count “after watching the original version a few days earlier fail and rewriting it” the first try.
So yeah, copy-on-write is annoying, but can be made fairly painless with a strong interface. It might be worth prepopulating my yapValue free list with some “spares” on VM initialization to help out with the potential yapValue thrashing that could happen on a heavily modified value.
I also reworked my grammar to properly support “assignment expressions”. My first pass of my grammar had assignments as statements, which means you can’t use them as an rvalue (the overall statement doesn’t offer a value). Now that this is done, these all work:
var a, var b, var c
a = b = c = 10
var a = var b = var c = 10
var a = 10, var b = 20, var c = 30
The biggest trick with chaining assignments was avoiding the recalculation of the value. Much like C macro argument side effects when passing in x++ on a macro that references the argument multiple times, I couldn’t just reassemble the child expression a second time to offer it to the parent expression, as it might be calling a function, changing some global state, etc. Instead, I mangled my setvar opcode to optionally not pop the value it used. Since the compiler is aware that the owner of this expression is requesting a value, it can set the setvar’s operand appropriately and you end up with some simple assembly output:
var a, var b, var c
a = b = c = 10
.kstr 0 "a"
.kstr 1 "b"
.kstr 2 "c"
.kint 0 10
The operand of 1 on the setvar statements indicates to the VM to leave that value on the stack after setting the variable reference. I should feel lucky that I had a spare operand to abuse! This grammar shake-up has prepared the code for list and dict (syntax like foo = 10).
Finally, I put in a boatload of operation and memory tracing in order to verify all of my copy-on-write shenanigans. I got really tired of stepping through the VM ops over and over, when all I really wanted was a log / blow-by-blow account of what the stack count was and when allocs/frees/acquires/releases happened.
Here’s the script from above with basic tracing on.
… and with value op tracing on.
… and with memory op tracing on.
The output can get a little ridiculous, but it was incredibly useful to make sure I was doing things correctly.
Now I just need to let myself sleep more.