OK @TheLoneWolfling this one took a while. I tried a few different approaches to clean up execute but none of them were clean. I finally settled on the function_names array you see below because it allows us to definitively separate native functions that manipulate the AST from core functions that just consume two values. We can also use the contains function I added (it's in pretty poor shape now but I'll fix it up) to create another native function for arrays and strings. Now that this is done I should be able to help you more with all the structures and whatnot.
I thought about a few different ways to make the execute function more abstract but I didn't find anything I liked at this point. I like how there are so few abstractions that it's really easy to see what's going on. At the very least execute.c is much smaller now and easy to deal with so we can improve other parts of the language without worrying too much about it.
Instead of the function names array, why not use a map instead? So you have a map of the various constants -> functions. Similar to how user-defined functions are handled, only with an actual C function being pointed to not an AST.
Make apply_execution_function return a struct Value , not a struct Value. Then you don't need the duplicate array check, as you can just add a fallthrough at the bottom of apply_execution_function to return null. (Or alternatively, have apply_execution_function return a bool and take a struct Value \ as an argument.)
OK @TheLoneWolfling this one took a while. I tried a few different approaches to clean up execute but none of them were clean. I finally settled on the function_names array you see below because it allows us to definitively separate native functions that manipulate the AST from core functions that just consume two values. We can also use the contains function I added (it's in pretty poor shape now but I'll fix it up) to create another native function for arrays and strings. Now that this is done I should be able to help you more with all the structures and whatnot.
I thought about a few different ways to make the execute function more abstract but I didn't find anything I liked at this point. I like how there are so few abstractions that it's really easy to see what's going on. At the very least execute.c is much smaller now and easy to deal with so we can improve other parts of the language without worrying too much about it.