¶Primitives should be primitive
¶2022-07-12
So far I've been trying to provide primitives as a regular Swanson unit, which you load just like any other. (Currently doing that via a “loader”, which is the only value passed in to a unit's entry point. Previously via a dependency injection kind of thing, where the entry point's inputs were the names of its dependencies, and the host took care of loading those before passing control to the entry point.)
Recently I was trying to figure out what a formal definition of the execution model would look like. And in that mathy world, I don't think it's appropriate to piggyback on module loading like that. Yes you can, but that doesn't mean that you should.
Instead, I'm thinking that it's more reasonable to have primitives actually be primitive. In a formalism, I'd represent this with another bag-of-crap in the context of the typing and operational semantics judgments. (Π, maybe?)
I can see two possibilities in the implementation. One is that entry points would take in a parameter called primitives. This would be an invokable, very much like the kernel unit that I'm currently providing. Again, we could make this work (we already are!), but I'm not sure that it's the best approach. It seems like it might be more cumbersome to implement, since you'd have to ensure linearity. (Each method would have to return the primitives invokable as a ‘$_’ result, you'd have to provide drop and clone methods, etc.)
The other possibility would be to have primitives be completely implicit, and add another terminal statement to the execution model. We already have “invoke”, which invokes a value in the environment. We'd add “invoke primitive”, which would invoke an implicit primitive method. (With “invoke”, the invocation target is removed from the environment before passing control. With “invoke primitive”, it wouldn't, since there isn't any value in the environment representing the primitives.) This approach is most aligned with how I anticipate the formalism version of this would work. And it seems like it would be easier to implement.
On the other hand, using a different terminal statement means you couldn't override the behavior of the primitives. It might be useful to invoke an entry point while passing in a different value to masquerade as the primitives. Maybe for test cases, maybe for having more control over the unit loader process, who knows.
There's a possible way to close the circle here, by (of course) adding a layer of indirection. Primitives would be latent, but units would still takes in a parameter. (Possibly called kernel to distinguish it from the latent primitives?) The kernel would use the special “invoke primitive” statement liberally. Other units typically wouldn't use the primitives directly, and would use the kernel instead. To override the behavior of the kernel, we'd make sure that you could locate a unit without loading it (i.e., getting its entry point as a value without invoking it), so that you could then invoke it with some other value that can masquerade as a kernel.