Having got [font='courier new']this[/font] working in destructors as per the last entry, the next challenge I decided to get into was making it possible to access values in the surrounding execution context from within the destructor body. Sounds simple, huh? Ha ha ha...
So readers of my recent entries may remember that Om allows access to non-local values such as:
var x = func{ var a = 10; var b = func { out a; }; b(); return b;};var z = x();z();
When the second from last line is run, [font='courier new']x[/font] is executed, and it calls [font='courier new']b()[/font]. During this invocation, [font='courier new']a[/font] exists so all is well but when the [font='courier new']b[/font] function is retrieved and called in isolation, at this point [font='courier new']a[/font] does not exist and the runtime has to be able to detect this and error out.This was solved by essentially detecting during compilation stage that a non-local access was being used, then using a special machine op code that searches a small uint array to find a unqiue function id that tells it where on the call stack the function info is located, so it can then use that to find the start point of the value stack that function is using and the local address of the value can be combined with this to retrieve the correct value. This was all described in detail a few posts back.
Now consider
var a = 10;if(true){ var x = { destructor = func { out a; }; };}
When the [font='courier new']if[/font] block exits, [font='courier new']x[/font] is destroyed, triggering the call to its destructor. However, as things stand a completely new execution context is created to run the destructor in so accessing [font='courier new']a[/font] was not possible.The system overview is something like this. You have [font='courier new']Om::Engine[/font] exposed to the user, which uses PIMPL to hide the fact it owns a [font='courier new']State[/font] object which represents the data shared between invocations of execution - the entity heap, text cache and so on. When some code is actually executed, a [font='courier new']Machine[/font] instance is created on the stack and its [font='courier new']execute()[/font] method called with the id of the function entity to be run.
Previously, the value stack, call stack and call id stack were all just members of the [font='courier new']Machine[/font] and so were unique to each call instance. When the [font='courier new']State::dec()[/font] method was called, a long chain of methods ends up happening whereby if it is an entity and its ref count goes to zero, the entity's virtual [font='courier new']release()[/font] method is called, which in the case of an Object, looked a bit like:
void ObjectEntity::release(State &s){ if(destruct.type() == Om::Type::Function) { s.execute(destruct.id); } // rest of release}
So the call to [font='courier new']State::execute[/font] creates a new [font='courier new']Machine[/font] to run the function, with its own value stack, call stack and so on. So from within this context, non-local access was impossible.It would have been a logical nightmare to try to make the objects destructor use the existing [font='courier new']Machine[/font] for a number of reasons, not least because there will in the future be the possibility for multiple [font='courier new']Machines[/font] to exist at once if scripts are running concurrently in the engine.
So instead, what I decided to do was to seperate out the stacks into an [font='courier new']ExecutionContext[/font], have a [font='courier new']Machine[/font] take a reference to an [font='courier new']ExecutionContext[/font] and do all its access via that reference. [font='courier new']ExecutionContext[/font] is just a container class for the stacks:
class ExecutionContext{public: ExecutionContext(); ExecutionContext(uint v, uint c); pod_vector vs; pod_vector cs; pod_vector cids;};
So now, in [font='courier new']State::execute[/font] for example, we do something like (very simplified):void State::execute(uint id){ ExecutionContext ec; Machine m(*this, ec, id); m.execute();}
Now the real work began. I had to make every call that was involved in a decrement of a value also take a reference to an [font='courier new']ExecutionContext[/font] so that when a decrement of an object in a [font='courier new']Machine[/font] happened, as it went down the chain of calls to eventually end up in [font='courier new']ObjectEntity::release[/font], the [font='courier new']ExecutionContext[/font] of the current machine was available.I can then create the new [font='courier new']Machine[/font] to invoke the destructor, passing in the reference to this [font='courier new']ExecutionContext[/font] so that the machine has access to the same value and call stacks as the [font='courier new']Machine[/font] that decremented the reference to the object. So non-local access now becomes possible.
If, on the other hand, something is retrieved from a script by return into an [font='courier new']Om::Value[/font] and the destructor ends up being called in isolation, it is instead executed by a different path that will create a blank [font='courier new']ExecutionContext[/font] for the machine, so the non-local access will correctly fail with the same message that the value is not available in the current context, much as happens when normal non-local access fails in script execution.
Had to make a few modifications to the [font='courier new']Machine[/font] to ensure that it coped with using an [font='courier new']ExecutionContext[/font] that contained data that that [font='courier new']Machine[/font] might not actually own, and took a vast amount of plumbing and rewriting to get it all in place, but it is finally working, so a destructor now has exactly the same semantics and access abilities as a normal function, dependant on the same rules of the context in which it is called.
But I have to be honest. I'm now considering a fourth complete rewrite. This whole unexpected need for so much to happen when an entity reference is decremented is not something I had considered at all at the start of this iteration as never got as far as destructors before and I'm starting to feel it is all a bit bolt-on now. I'm trying to decide if I can do a more efficient job if I start again at this point with this in mind from the start, or if I'll end up with much the same as what I have already.
The bolt-on nature means lots of passing of things around by reference and accessing things via references, all of which has overhead which, however minor, may be avoidable by doing a complete rewrite with this in mind from the beginning. Will have to think about this for a bit because a lot of work has gone into this iteration, but if a new start will make a better runtime, I'll bite the bullet and do it.
Thanks for sticking with me and reading as usual.