I finished the pipeline from my language’s AST to scratch, it was pretty mind numbing
Stage 1: The code does reachability, from the source file(where the compilation started) and finds all functions that we can call from the source
Stage 2: Using those functions, we lower call expressions to statements and returns to heap accesses, here’s how that looks in code:
Before:
void main() {
looks::say(fib(5));
}
After:
void main() {
int fibReturn = -1;
fib(5, &fibReturn);
looks::say(fibReturn);
}
Notice how the nested call turned into a seperate statement. Why do we need this? Because scratch does not support returning stuff from functions, only arguments.
Stage 3: We re-parse all locals on the reachable functions because the last stage added more locals
Stage 4: We add a parameter to all functions named “stack”
Stage 5: We add free(stack) calls at the end or before the return statement, this frees our stack automatically.
We also add alloc before function calls so that we have a stack to pass to the function
Here’s how that looks like in code:
Before:
void main() {
int fibReturn = -1;
fib(5, &fibReturn);
looks::say(fibReturn);
}
After:
void main(int stack) {
int fibReturn = -1;
int fibStack = -1;
alloc(UNKNOWN, &fibStack); //allocate a stack for fib
fib(fibStack, 5, &fibReturn);
looks::say(fibReturn);
free(stack, UNKNOWN); //free our stack
}
Notice how we have UNKNOWN in place for a number in the allocation and freeing, this is because we just added more locals for the allocations and we can’t know how many slots we need.
Standard library functions are already compiled to scratch statements so they don’t need a stack allocated for them
Stage 6: We re-parse local variables so we have a count.
Stage 7: We count the re-parsed locals for every function and figure out which local variables can be reached in every function
Stage 8: We convert all local variables to heap slots
We traverse all statements and expressions, replacing all variable accesses with heap[stack + variableIndex].
While in this process we also convert the unknowns into the actual sizes since we just counted the locals in stage 7
Here’s how this entire stage looks like in code:
Before:
void main(int stack) {
int fibReturn = -1;
int fibStack = -1;
alloc(UNKNOWN, &fibStack);
fib(fibStack, 5, &fibReturn);
looks::say(fibReturn);
free(stack, UNKNOWN);
}
After:
void main(int stack) {
heap[stack] = -1;
heap[stack + 1] = -1;
alloc(3, stack + 1); //we just pass the index here since alloc will write to that index
fib(heap[stack + 1], 5, stack);
looks::say(heap[stack]);
free(stack, 2); //We have 2 locals
}
By using a stack we get access to a lot of things like returning, recursion and local variables. Scratch doesn’t even support local variables, so we have to hack it with a global list.
Notice how we don’t free the allocation? That’s because all functions are compiled by the same compiler and that compiler added free() to all functions with a stack.
Stage 9: If a function doesn’t have any local variables, this is where we delete their allocations and free’s so that they don’t mess up the heap
For example:
alloc(0, index);
and the target function for this allocation will have its free(stack, 0) call removed.
Stage 10: Finally we create dummy scratch functions for our very lowered AST
And finally in stage 11 we create the code blocks for the scratch functions and write to disk
And it successfully calculates fib(9) as 34
Comments 0
No comments yet. Be the first!
Sign in to join the conversation.