The problem, in particular, is that we can't do a naive traversal of the code tree to detect all side effects. As long as we don't use any higher-order functions, it's fine and actually fairly simple to ensure that all side effects are contained correctly.
However, as soon as a higher-order function is involved, we run into problems. Consider this simple case:
entrypoint : () -> (){ task(async1) { foobar() }}foobar : () -> (){ shellfunction(borken) }shellfunction : (function op : () -> ()) -> (){ op()}borken : () -> (){ debugwritestring("Oh noes! We borked the task checker!")}
The debugwritestring operation is flagged as unsafe for using from tasks, since it may cause contention for the console. Normally, the usage of an unsafe function would be caught, and a compile-time error would be thrown.
However, since we only invoke the borken function indirectly via a higher-order function, the error is missed. At compile time, the op parameter of shellfunction is not bound to anything, so there is no way to tell what function might be passed. Without any bindings, the function call is ignored, and the program traversal walks right past the obvious error without even blinking.
The solution, of course, is to build a tree of higher-order function calls and test every potential code path that might invoke such a function. This may prove complicated and even slow, but it only needs to be done once - at compile time - and at runtime things will be just as simple and fast.
(Technically, because of the way the VM is architected, you can actually do illegal things by writing EpochASM code directly, or even bytecode. Since a lot of safety checks are only done at compile time, you can do some pretty cool hacks if you're willing to get dirty and play with the VM's guts.)
So my big challenge of the day is to come up with a way to handle the higher-order function case during the validation traversal.
Aside from that one hangup, tasks are working great. My goal is to get message passing implemented within the next week, so I have a few days spare to polish everything up and write the pitch documents for GDC.
E.g. debugstring is unsafe, so borken is unsafe. borken is referenced in foobar, so foobar is unsafe. Therefore, the code should regurgitate an 'unsafe'. So really, you just need 1 unsafe flag per function. Start at the bottom of your AST and work your way up...
Then again, this also would assume that anytime you have 'f(a)' in your code, and you generate your AST, you aren't creating a new node for f. Instead, all nodes that contain f point to the same f node. So in your example, all references in the AST to 'borken' would point to the same function node. It requires a bit more 'linking' of the tree, but makes propagating 'unsafe' markers up pretty easy.