While writing some error handling helpers, during my algorithm stream, I encountered a defect. It's a tricky one dealing with the passing of shared arguments to functions in Leaf.
After a lot of searching I managed to reduce it to this minimal unit test.
/* EXPECT(3) */
var a : integer shared := 2
defn pine = (b : integer shared) -> {
var c : integer shared := 3
b = c
trace(b)
}
pine(a)
The function is silly, but it is sufficient to demonstrate an error. It manifests as severe sounding abort: duplicate-release-shared
error.
For the curious, the example was reduced from the below error reporting function.
@export defn print = ( ofi : ⁑fail_info optional shared ) -> {
println(["Error:"])
while has(ofi) {
var q = getopt(ofi)
println(["\t",q.tag])
ofi = q.next
}
}
That iterates over the tags in an error and prints them to the console. It mostly works, except for one issue on the ofi = q.next
line -- it also causes that nasty duplicate-release-shared
error.
It's a problem of ownership
The error is one of ownership: two code paths are freeing the same value. Consider this basic code:
var b : shared integer := 5
var c : shared integer := 6
b = c
b
and c
are both shared values. These are roughly equivalent to a shared_ptr<integer>
in C++, or the Integer
type in Java. When we assign b = c
we're not modifying values, rather changing references. You may refer to my article on divorcing a name from its value for more details on that concept.
Leaf uses reference counting; this is roughly what happens when we assign to a shared variable:
// b = c
acquire(c)
release(b)
assign(b, c)
We release the old object in b
, acquire the new one c
, and then assign the reference (a memory pointer).
Take a look back at the test case again, the b = c
comes from that example:
defn pine = (b : integer shared) -> {
var c : integer shared := 3
b = c
trace(b)
}
The trouble here is that b
is an argument to the function, so we're only allowed to release
it if we own the variable. It turns out we don't!
Consider the calling side:
pine(a)
Reduced to pseudo-code, again based on reference counting:
var a_tmp = acquire(a)
pine( a_tmp )
release( a_tmp )
This bit of code ensures that the reference to a
is valid during the function call. Since a
is a globally modifiable, we need to play it safe here. The problem is the above code calls release(a_tmp)
. But our pine
function is also doing release(b)
, which happens to be the same shared object as a_tmp
. Thus we've released the object twice!
Logically avoiding the issue
This issue can be avoided by shadowing all arguments locally in the function.
defn pine = ( _b : integer shared) -> {
var b = _b
var c : integer shared := 3
b = c
trace(b)
}
The shadowing works since the first thing the function does is acquire(_b)
, and the eventual release(b)
doesn't touch the source argument.
It feels a bit like overkill to do this for all arguments on the off-chance they might be modified. I guess the compiler could scan the code and figure it out. Or, I could just disallow modifying function arguments. That's how I arrived at my question Should function arguments be reassignable or mutable? .
For sanity in the compiler, I'll likely add a fix (local shadowing), and forbid assigned to arguments by default. Then use some magic to try and optimize away all situations where it isn't needed -- though popular opinion seems to lean towards forbidding the modification of arguments.
Be sure to visit the Leaf site to learn more about my programming language. You can also follow me on Twitter or subscribe to my live stream and feel free to ask questions.
Top comments (0)