Compare commits
No commits in common. "692ed4d21cc1d7c0a8e2f3e8fdd7af8109baf353" and "b21ce514357093a9f42d9de9bc34e489d3b01430" have entirely different histories.
692ed4d21c
...
b21ce51435
118
DOCUMENTATION.md
118
DOCUMENTATION.md
@ -4,68 +4,55 @@
|
||||
|
||||
When writing a program, I usually make the most primitive and smallest code I can that does the job. If it turns out I miscalculated the complexity, or I must add some feature that isn't compatible with the codebase, I'll obviously have to refactor it. Still, I've been programming this way for probably my entire life.
|
||||
|
||||
That being said, if you know this compiler took since 2019 to get to its current state, you will correctly guess that I DO NOT KNOW WHAT I AM DOING. Compiler literature and online discussion is abstract to the point where it is not useful for real-world archs. When it gets specific, it's often too simplistic. It's common to say instruction selection should happen before register allocation, but how can you know which instructions to emit when some of them only work with specific registers? People say spilling causes load and store instructions, but that's false -- x86 has memory operands! Imagine how long it took me to realize real-world IRs are not at all generic, and are actually quite close to their target architectures. As a result, much of what you see in the source is me basically banging rocks. There's definitely better ways to do the things I show here, but I figured it's better to have at least some resource on how a "real" compiler works.
|
||||
That being said, if you know this compiler took since 2019 to get to its current state, you will correctly guess that I DO NOT KNOW WHAT I AM DOING. Compiler literature and online discussion is abstract to the point where it is not useful for real-world archs. When it gets specific, it's often too simplistic. It's common to say instruction selection should happen before register allocation, but how can you know which instructions to emit when some of them only work with specific registers? People say spilling causes more loads and stores, but that's false; x86 has memory operands! Imagine how long it took me to realize real-world IRs are not at all generic, and are actually quite close to their target architectures. As a result, much of what you see in the source is me basically banging rocks. There's definitely better ways to do the things I show here, but I figured it's better to have at least some resource on how a "real" compiler works.
|
||||
|
||||
The core idea behind the compiler is to progressively iterate through the AST, turning it into a more primitive form step by step. After this process, the code generator is given the code in a form it will understand. Doing it this way is necessary because machine code itself is primitive, and instructions typically have 0-3 operands. Thanks to both this, and Nectar itself being highly low-level, the need for an IR disappears.
|
||||
The core idea behind the compiler is to progressively iterate through the AST, turning it into a more primitive form step by step. Once this primitivization ends, the code generator is given the code in a form it will understand. Doing it this way is necessary because machine code itself is primitive, and instructions typically have 0-3 operands. Thanks to both this, and Nectar itself being highly low-level, the need for an IR disappears. On the other hand, making sure the AST is in a correct state between steps is the prime source of bugs.
|
||||
|
||||
But make no mistake: this approach does not simplify the compiler! At best this moves the complexity to the normalization. Along each step you must make sure the AST is in a valid state. Almost every bug will be silent, and will manifest as misbehavior or malformed results.
|
||||
|
||||
Currently the compiler is designed with only i386+ processors in mind. I intend to add support for i286- and other exotic processors, but I honestly don't see it happening ever, especially if this remains a solo project. RISCier architectures with regular register files will be easier to add support for, but as they're likely designed with C in mind, the advantages of this programming language aren't worth the squeeze.
|
||||
Currently the compiler is designed with only i386+ processors in mind. I intend to add support for i286- and other exotic processors, but I honestly don't see it happening ever, especially if this remains a solo project. More RISC architectures with regular register files will be easier to add support for, but they're also the kind for which the advantages of this programming language aren't worth the squeeze.
|
||||
|
||||
## AST structure
|
||||
|
||||
Starting with a Nectar source file, the compiler begins with the common passes: lexing, skimming and parsing. Skimming exploits quirks in Nectar's syntax, and may jump back and forth multiple times to find all symbols and types, as an automatic forward declaration. At the end, parsing returns what is called an AST in the source.
|
||||
Starting with a Nectar source file, the compiler begins with the two common passes: lexing and parsing. The skimming in-between exploits Nectar's syntax quirks, and may jump back and forth multiple times to find all declared symbols and types in advance. This is necessary to avoid having to forward declare items. At the end, parsing returns what is called an AST in the source.
|
||||
|
||||
An AST node may not be shared by multiple parent nodes. Also, the internal Nectar AST does not have scaling for pointer arithmetic; all pointers behave as `u8*`. This is the first of many simplifications.
|
||||
|
||||
Each block of code between `{` and `}` is called a "chunk", a term from Lua. Chunks may contain one another; the highest one within a function is called the top-level chunk (TLC). During parsing of a chunk, definitions of vars/symbols are collected into the relevant `Scope`. After a chunk is parsed, all local variables in the chunk's `Scope` are copied to the TLC's list of local variables, for subsequent analysis.
|
||||
Each block of code is called a "chunk", likely a term I took from Lua. Chunks may contain one another; the least deep one within a function is called the top-level chunk (very important). Top-level chunks may contain other top-level chunks, because user-defined functions are within the "global scope", which is considered a function in itself. After all, nothing stops you from directly inserting instructions in the `.text` section of an executable, without attaching it to a label.
|
||||
|
||||
However, top-level chunks may contain other top-level chunks, because user-defined functions are within the "global scope", which is considered a function in itself. After all, nothing stops you from directly inserting instructions in the `.text` section of an executable, without attaching it to a label.
|
||||
During parsing, a tree of maps, `Scope`, is used to handle scopes for variables, types, symbols and constant expressions. Its entries are of type `ScopeItem` (often called VTE in the source for historical reasons). Shadowing of scope items is allowed, like in Nectar itself.
|
||||
|
||||
There's enough passes to justify a generic way to invoke the visitor pattern on the AST. Because passes may do many different things to the AST, including modify it, the definition of a generic visitor is very broad. Most functionality is unused by each pass, but all of it is needed.
|
||||
The top-level chunk keeps a list of variables within its `ASTChunk` structure. After a chunk is finished parsing, all local variables in the current `Scope` are added to its top-level chunk's variable list. Names may conflict, but at this point they're no longer important. Also worth mentioning is that this flat list contains `ScopeItem` structs, even though `Scope`s are now irrelevant. Said items are all of type `SCOPEITEM_VAR`; the rest are ignored because they're not subject to coloring.
|
||||
|
||||
There's enough types of passes to push us to have a generic way to invoke the visitor pattern on the AST. Because passes may do many different things to the AST, including modify it, the definition of a generic visitor is very broad. Most functionality is unused by each pass, but all of it is needed.
|
||||
|
||||
void generic_visitor(AST **nptr, AST *stmt, AST *stmtPrev, AST *chu, AST *tlc, void *ud, GenericVisitorHandler preHandler, GenericVisitorHandler postHandler);
|
||||
|
||||
`*nptr` is the actual node that is currently being visited. It is behind an additional indirection, because the node may be replaced by another.
|
||||
|
||||
If the current node is within a statement (most are), `stmt` is equal to that statement. `stmtPrev` is the previous statement. This is necessary in case a pass generates new statements, patching in the linked list of statements. If there is no previous statement, then the head pointer of the list must be patched through the `chu` node. `tlc` is the top-level chunk, which may be equal to `chu`.
|
||||
If the current node is within a statement (most are), `stmt` is equal to that statement. `stmtPrev` is the previous statement. This is necessary for patching in the linked list of statements within a chunk during modification passes. If there is no previous statement, then the head pointer of the singly-linked list must be patched through the `chu` node. The `tlc` is the top-level chunk, which may be equal to `chu`.
|
||||
|
||||
A handler may be called before (pre) or after (post) delving deeper into the tree. Most passes use the prehandler, but type checking will be better with a posthandler, since we want type checks to happen bottom to top.
|
||||
A handler may be called before or after delving deeper into the tree (hence the pre and post handlers). Most passes use the prehandler, but type checking will be better with a posthandler, since we want type checks to happen bottom to top.
|
||||
|
||||
## Desegmentation
|
||||
|
||||
Pointers in Nectar might not be regular integers. The basic (near) ones are, but we also have far pointers to support x86 segmentation. Internally these are record types with two fields: a `u16` segment and a `T*` near pointer.
|
||||
|
||||
`ast_segmented_dereference` takes each instance of a far pointer dereference and converts it to a segment load & near pointer dereference. For example, with a `u8 @far* x;`, `y = *x` becomes:
|
||||
|
||||
$segtemp_0 = x.segment;
|
||||
y = *x.offset;
|
||||
|
||||
`$segtemp_0` is colored to `ds`. Other segment registers exist in x86; they're unsupported for now.
|
||||
|
||||
There's also huge pointers, but they're basically far pointers + some QoL.
|
||||
Pointers in Nectar might not be regular integers. The basic ones (called near) are, but we also have far pointers to support x86 segmentation. Internally these are record types with two fields: a `u16` segment and a `T*` near pointer.
|
||||
|
||||
## SRoA
|
||||
|
||||
A record variable that needn't be stored in memory should be split into the fields that compose it. This can help produce faster machine code. SRoA is handled by `src/ast/sroa.c`.
|
||||
## Pre-dumbification
|
||||
|
||||
The decision to split a record depends on two factors: the record size and whether the record is ever referenced with `&`. A superfluous `&` is harmful for SRoA and should be removed by prior optimization passes, but this is unimplemented.
|
||||
|
||||
## Pre-normalization
|
||||
|
||||
We make some one-time changes to the AST that normalization itself shouldn't take care of.
|
||||
Before dumbification we need to make sure the code at least matches the semantics of the x86 architecture.
|
||||
|
||||
For one thing, arguments are not magically loaded with the correct values. The `pre_dumb_visitor` pass inserts assignment statements at the beginning of the function to load the values from the stack, as per the C ABI.
|
||||
|
||||
Then we have structures which, of course, x86 doesn't support. There must be no by-value use of a structure anywhere. The `decompose_symbol_record_field_access` pass decomposes all references to a structure's field to pointer-like accesses. For example, `a.b` becomes `*((&a + x) as B*)`, where `B` is the type of `a.b`, and `x` is the memory offset of the field `a.b`. The same pattern is specially recognized by the code generator for outputting an x86 address mode like `[eax + x]`.
|
||||
|
||||
Afterward, local structs must be either spilled to global memory or to the stack. This is done by `ast_spill_to_stack`. Since spilling causes the stack frame of the function to grow, all references to the stack must be updated accordingly. This is also done by `spill2stack_visitor`, and in such a way so as to not require more normalization.
|
||||
Afterward, local structs must be either spilled to global memory or to the stack. They may also be decomposed in some cases, but that is currently unsupported. This is done by `spill2stack_visitor`, with `ast_spill_to_stack` as the public interface. Since spilling causes the stack frame of the function to grow, all references to the stack must be updated accordingly. This is also done by `spill2stack_visitor`, and in such a way so as to not require more dumbification.
|
||||
|
||||
Now a deeper structure access like `a.b.c` (`*((&(*((&a + x) as B*)) + y) as C*)`) is not recognized by the code generator. We must rely on optimizations passes that are guaranteed to happen: `*&p` and `&*p` should become `p`, `p as A* as B*` should become `p as B*` and `(p + x) as A* + y` should become `(p + (x + y)) as A*`. These are done by `ast_denoop`.
|
||||
Now a deeper structure access like `a.b.c` (`*((&(*((&a + x) as B*)) + y) as C*)`) is not recognized by the code generator. We must rely on optimizations passes that are guaranteed to happen: `*&p` and `&*p` should become `p`, `p as A* as B*` should become `p as B*` and `(p + x) as A* + y` should become `(p + (x + y)) as A*`. These are done by `denoop_visitor`.
|
||||
|
||||
## Normalization
|
||||
## Dumbification
|
||||
|
||||
The idea of turning the AST progressively primitive is called "normalization" in the source. The most simple example would be the following:
|
||||
The idea of turning the AST progressively primitive is called "dumbification" in the source. The most simple example would be the following:
|
||||
|
||||
a = -b
|
||||
|
||||
@ -74,13 +61,13 @@ which should become
|
||||
a = b
|
||||
a = -a
|
||||
|
||||
Because the `neg` instruction on x86 is single-operand. If targeting an arch like MIPS, this specific normalization would not be used, because one can use the 3-operand `subu` with the zero register.
|
||||
Because the `neg` instruction on x86 is single-operand. If targeting an arch like MIPS, this specific dumbification would not be used, because one can use the 3-operand `subu` with the zero register.
|
||||
|
||||
Another rule is to extract function arguments and place them into local variables, but *only* if they do not form an x86 operand (for example `5` is ok because `push 5` exists).
|
||||
|
||||
Normalization must be repeated until there are no more changes. The normalization part of the source is responsible for making sure the resulting AST is "trivially compilable" to the machine code. For example, `a = a + b` is trivially compilable, because we have the `add reg, reg` instruction. What is trivially compilable depends on which registers are used in the end (a variable colored as `edi`, `esi` or `ebp` cannot be used for 8-bit stores/loads). These details are not taken into account by normalization.
|
||||
Dumbification must be repeated until there are no more changes. The dumbification part of the source is responsible for making sure the resulting AST is "trivially compilable" to the machine code. For example, `a = a + b` is trivially compilable, because we have the `add reg, reg` instruction. What is trivially compilable depends on which registers are used in the end (a variable colored as `edi`, `esi` or `ebp` cannot be used for 8-bit stores/loads). These details are not taken into account by dumbification.
|
||||
|
||||
Putting all of this together, here is an example of nctref's normalization of the following Fibonacci implementation, as of writing. Here is the main Nectar source code:
|
||||
Putting all of this together, here is an example of nctref's dumbification of the following Fibonacci implementation, as of writing. Here is the main Nectar source code:
|
||||
|
||||
fibonacci: u32(u32 n) -> {
|
||||
if(n <= 1) {
|
||||
@ -108,6 +95,8 @@ And the processed AST output by the compiler:
|
||||
|
||||
`@stack` is an internal variable that points to the beginning of the current stack frame.
|
||||
|
||||
NOTE: Later someone called this normalization, which is a much less stupid word than dumbification, and I'm shocked I never thought of it myself. There's also canonicalization...
|
||||
|
||||
## Use-def chain
|
||||
|
||||
***WARNING: THIS ENTIRE SECTION HAS BECOME OUTDATED***
|
||||
@ -200,17 +189,19 @@ The actual coloring algorithm used is Welsh-Powell, which sorts the VTEs/vertice
|
||||
|
||||
If there's more colors than there are physical registers, then we have a conflict, and must spill. `spill2stack` transforms every use of a local variable (`ASTExprVar` where its VTE is of type `SCOPEITEM_VAR`) into the form `@stack + n`.
|
||||
|
||||
If spill2stack is used, then CG must fail once so that normalization can be applied again.
|
||||
If spill2stack is used, then CG must fail once so that dumbification can be applied again.
|
||||
|
||||
## Pre-coloring
|
||||
|
||||
NOTE: `spill2var` turned out to be pushing the problem a step back rather than solving it. Because it is known in advance what must be pre-colored, any such expressions are immediately placed in their own variable by another pass. If the assignment turns out to have been redundant, the register allocator should coaslesce the moves.
|
||||
|
||||
Pre-coloring is a necessary evil. It is *very* common in x86 that an instruction's operand is some fixed register we cannot change. But even worse: pre-coloring cannot be handled by
|
||||
TODO: preclassing.
|
||||
|
||||
## Callee-saved pass
|
||||
|
||||
If a function uses a callee-saved register, these must be stored and loaded at the correct times. This is done by `callee_saved` in `src/x86/cg.c`. If the hardware resources used by any variable overlaps with those of a callee-saved register, said register is marked. Afterward, these registers are passed to the code generator.
|
||||
If a function uses a callee-saved register, these must be stored and loaded at the correct times. This is done by modifying the AST in a special pass.
|
||||
|
||||
Of the four currently used registers, only `ebx` is callee-saved. A random variable colored `ebx` is chosen, and is saved to/loaded from the stack. The rule is written such that dumbification isn't necessary, unlike spill2stack.
|
||||
|
||||
## Code generation
|
||||
|
||||
@ -296,7 +287,7 @@ This splits the pointer type into three subtypes:
|
||||
|
||||
By default, pointers in Nectar are just integers, i.e. near, limiting a Nectar program to only 64kB. The parameter `mem` sets all unspecified pointers in a Nectar source file to huge pointers.
|
||||
|
||||
Far and huge pointers have to be normalized. Let us have the following:
|
||||
Far and huge pointers have to be dumbified. Let us have the following:
|
||||
|
||||
u8 @far* a;
|
||||
|
||||
@ -323,11 +314,16 @@ This example benefits from scalar replacement:
|
||||
|
||||
a_offset = a_offset + 1;
|
||||
|
||||
If `a` were `u8 @huge*`, we must account for overflow and the last statement would instead become:
|
||||
|
||||
a.offset = a.offset + 1;
|
||||
if(a.offset == 0) {
|
||||
a.segment = a.segment + 4096;
|
||||
}
|
||||
|
||||
## Other problems with this approach (1)
|
||||
|
||||
In the past, a function in the AST was essentially a tree of blocks, following the "structured programming" of Nectar source code. This was a poor choice for multiple reasons.
|
||||
|
||||
Short-circuit evaluation is when the evaluation of an expression is guaranteed to stop once the output is already known. For example, if in `A || B` `A` is already truthy, then `B` is not evaluated. This is not an optimization, but an important semantical detail.
|
||||
Short-circuit evaluation is when the evaluation of an expression is guaranteed to stop once the output is already known. For example, if in `A || B` `A` is already truthy, then `B` is not evaluated. This is not an optimization, but an important semantical detail, as evaluation of the operands may have side-effects.
|
||||
|
||||
Let us write `if(x == 1 || y == 1) { do stuff; }` in x86:
|
||||
|
||||
@ -339,9 +335,9 @@ Let us write `if(x == 1 || y == 1) { do stuff; }` in x86:
|
||||
; do stuff
|
||||
.L2:
|
||||
|
||||
Note that the two jump instructions are basically goto statements. With a tree of blocks, this kind of logic isn't feasible.
|
||||
Note that the two jump instructions are basically goto statements. As the Nectar IR is defined without gotos, it is practically impossible for the compiler to output the neat code shown above. You could insert special logic for this case, but in general it'll fail.
|
||||
|
||||
Even worse, normalization will try to move the condition into a variable and normalize it further, creating the following:
|
||||
Even worse, the dumbification pass will try to move the condition into a variable and dumbify it further, creating the following:
|
||||
|
||||
u1 z = x == 1;
|
||||
u1 w = y == 1;
|
||||
@ -352,11 +348,35 @@ Even worse, normalization will try to move the condition into a variable and nor
|
||||
|
||||
And now we need 2 new registers for no reason..
|
||||
|
||||
What else? Did you know `return` statements are gotos when a function is inlined, too? In other words, no function inlining.
|
||||
Lack of gotos also makes function inlining impossible, as returns also become gotos (!!).
|
||||
|
||||
In conclusion, what? Should a good IR actually be 100% flat and have nothing but jumps? Can this be solved by modelling the code as a graph of basic blocks? I don't know, but for now I have given up on short-circuit evaluation, and I do not actually support neither `||` nor `&&`.
|
||||
|
||||
## Other problems with this approach (2)
|
||||
|
||||
The `denoop_visitor` pass is incredibly important in normalizing the AST to something other passes will accept. Here's one case I found when trying to implement a statically allocated list class:
|
||||
|
||||
T* data = &((*this).data[0]);
|
||||
|
||||
It seems innocent enough, but it actually becomes:
|
||||
|
||||
T* data = &*(&*((&*this + 4) as T[4]*) + 0);
|
||||
|
||||
As of writing, `denoop_visitor` had produced this:
|
||||
|
||||
T* data = (this + 4) as T*;
|
||||
|
||||
The code generator failed to accept this, because the `as T*` cast meant that it could not match any pattern. The dumbifier also failed to decompose this to `data = this; data = data + 4;` for the same reason.
|
||||
|
||||
What was my solution? IGNORE ALL POINTER CASTS! As I wrote above, the Nectar AST does not support pointer arithmetic like that of C. By this point, all complex types should have already been converted into integers. Therefore, it does not even matter.
|
||||
|
||||
By adding the rule (`x as A*` -> `x` if x is of a pointer type), we obtain the following after denooping:
|
||||
|
||||
T* data = this + 4;
|
||||
|
||||
## Adding a Feature
|
||||
|
||||
When adding a feature, first write it out in Nectar in the ideal normalized form. Make sure this compiles correctly. Afterward, implement normalization rules so that code can be written in any fashion. If specific colorings are required, then the pre-coloring and spill2var passes must be updated. The following is an example with multiplication, as this is what I'm adding as of writing.
|
||||
When adding a feature, first write it out in Nectar in the ideal dumbified form. Make sure this compiles correctly. Afterward, implement dumbification rules so that code can be written in any fashion. If specific colorings are required, then the pre-coloring and spill2var passes must be updated. The following is an example with multiplication, as this is what I'm adding as of writing.
|
||||
|
||||
Note the way `mul` works on x86 (yes, I'm aware `imul` exists). Firstly, one of the operands is the destination, because `mul` is a 2-op instruction. Secondly, the other operand cannot be an immediate, because it is defined as r/m (register or memory), so if the second operand is a constant, it must be split into a variable (`varify` in `dumberdowner.c`). Thirdly, the destination must be the A register, so one of the operands must be pre-colored to A. Fourthly, `mul` clobbers the D register with the high half of the product. In other words, we have an instruction with *two* output registers, which the Nectar AST does not support. But we can't have the register allocator assign anything to D here.
|
||||
|
||||
@ -366,22 +386,22 @@ To account for this, we can have a second assignment statement right next to the
|
||||
w = z *^ y;
|
||||
z = z * y;
|
||||
|
||||
But this is without pre-coloring. We want precolored nodes to live as little as possible, because separately solving pre-coloring collisions whilst also keeping the code normalized *and* not horrible turned out to be practically impossible (spill2var).
|
||||
But this is without pre-coloring. We want precolored nodes to live as little as possible, because separately solving pre-coloring collisions whilst also keeping the code dumbified *and* not horrible turned out to be practically impossible (spill2var).
|
||||
|
||||
k = x;
|
||||
w = k *^ y;
|
||||
k = k * y;
|
||||
z = k;
|
||||
|
||||
Where `k` and `w` are the VTEs that must live and die immediately. The normalizer checks if `w` and `k` are already precolored to not loop forever, but it would be better to check the UD-chain.
|
||||
Where `k` and `w` are the VTEs that must live and die immediately. The dumbifier checks if `w` and `k` are already precolored to not repeat forever, but it would be better to check the UD-chain.
|
||||
|
||||
Lastly, the codegen pass must recognize the above sequence as a multiplication and emit a single `mul` instruction.
|
||||
|
||||
In `cg.c` is a function called `xop`, which returns an x86 operand string, given a trivially compilable Nectar expression. Because we've guaranteed the other operand may not be a constant, we do not need to check the XOP type, but it's a good idea to insert `assert`s and `abort`s everywhere to prevent hard-to-find bugs.
|
||||
|
||||
Once all that is done and tested, now we can add the following normalization rules: all binary operations with the operand `AST_BINOP_MUL` or `AST_BINOP_MULHI` must be the whole expression within an assignment statement. If not, extract into a separate assignment & new variable with `varify`. The destination of the assignment, and both operands of the binary operation must be of type `AST_EXPR_VAR`, with their corresponding variables being of type `SCOPEITEM_VAR`, not `SCOPEITEM_SYMBOL`, `SCOPEITEM_TYPE` nor `SCOPEITEM_CEXPR`. If any of those don't apply, `varify` the offenders. Each such assignment have a neighboring, symmetric assignment, so that both A and D are caught by the pre-coloring pass.
|
||||
Once all that is done and tested, now we can add the following dumbification rules: all binary operations with the operand `AST_BINOP_MUL` or `AST_BINOP_MULHI` must be the whole expression within an assignment statement. If not, extract into a separate assignment & new variable with `varify`. The destination of the assignment, and both operands of the binary operation must be of type `AST_EXPR_VAR`, with their corresponding variables being of type `SCOPEITEM_VAR`, not `SCOPEITEM_SYMBOL`, `SCOPEITEM_TYPE` nor `SCOPEITEM_CEXPR`. If any of those don't apply, `varify` the offenders. Each such assignment have a neighboring, symmetric assignment, so that both A and D are caught by the pre-coloring pass.
|
||||
|
||||
A common bug when writing a normalization rule is ending up with one that is always successful. If this happens, the compiler will become stuck endlessly normalizing, which is nonsense. It would be nice if you could formally prove that won't happen. Another common bug is not realizing the order in which normalization rules are applied matters :).
|
||||
A common bug when writing a dumbification rule is ending up with one that is always successful. If this happens, the compiler will become stuck endlessly dumbifying, which is nonsense. It would be nice if you could formally prove that won't happen. Another common bug is not realizing the order in which dumbification rules are applied matters :).
|
||||
|
||||
You know, I really regret writing this in C.
|
||||
|
||||
|
@ -1,112 +0,0 @@
|
||||
/*
|
||||
* MapDQCOaLDhS: Dynamically-allocated, Quadratic growth, C allocator, Interleaved KV values, Open-addressing, Linear probing, Dynamic hash, Flag tombstones
|
||||
*/
|
||||
|
||||
extern u8*(u8*, ugpr) calloc;
|
||||
|
||||
record KVPair[K, V] {
|
||||
K key;
|
||||
V value;
|
||||
}
|
||||
|
||||
record MapDQCOaLDhS[K, V, S] {
|
||||
S(K*)* hash;
|
||||
|
||||
S capacity;
|
||||
KVPair[K, V][?]* data;
|
||||
u8[?]* occupied;
|
||||
}
|
||||
|
||||
MapDQCOaLDhS_try_add: [K, V, S]u1(MapDQCOaLDhS[K, V, S]* this, K* key, V* value) -> {
|
||||
if(this.capacity == 0) {
|
||||
this.capacity = 64;
|
||||
this.data = calloc(this.capacity, @sizeof KVPair[K, V]);
|
||||
this.occupied = calloc(this.capacity, @sizeof((*this.occupied)[0]));
|
||||
}
|
||||
|
||||
S capacity = this.capacity;
|
||||
|
||||
S start = this.hash(key);
|
||||
start = start & (capacity - 1);
|
||||
|
||||
S index = start;
|
||||
loop {
|
||||
KVPair[K, V]* pair = &(*this.data)[index];
|
||||
if(pair.key == *key || (*this.occupied)[index] == 0) {
|
||||
pair.key = *key;
|
||||
pair.value = *value;
|
||||
(*this.occupied)[index] = 1;
|
||||
return 1;
|
||||
}
|
||||
index = (index + 1) & (capacity - 1);
|
||||
if(index == start) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
MapDQCOaLDhS_expand: [K, V, S]u1(MapDQCOaLDhS[K, V, S]* this) -> {
|
||||
/* Unimplemented. */
|
||||
return 0;
|
||||
};
|
||||
|
||||
MapDQCOaLDhS_add: [K, V, S]u1(MapDQCOaLDhS[K, V, S]* this, K* key, V* value) -> {
|
||||
loop {
|
||||
if(MapDQCOaLDhS_try_add[K, V, S](this, key, value) != 0) {
|
||||
return 1;
|
||||
}
|
||||
if(MapDQCOaLDhS_expand[K, V, S](this) == 0) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
MapDQCOaLDhS_get: [K, V, S]V*(MapDQCOaLDhS[K, V, S]* this, K* key) -> {
|
||||
S capacity = this.capacity;
|
||||
|
||||
if(capacity == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
S start = this.hash(key);
|
||||
start = start & (capacity - 1);
|
||||
|
||||
S index = start;
|
||||
loop {
|
||||
KVPair[K, V]* pair = &((*this.data)[index]);
|
||||
if(pair.key == *key) {
|
||||
if((*this.occupied)[index] == 0) {
|
||||
return null;
|
||||
}
|
||||
return &pair.value;
|
||||
}
|
||||
index = (index + 1) & (capacity - 1);
|
||||
if(index == start) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
zero_hash: ugpr(ugpr* val) -> {
|
||||
return 0;
|
||||
};
|
||||
|
||||
@instantiate MapDQCOaLDhS_try_add[ugpr, ugpr, ugpr];
|
||||
@instantiate MapDQCOaLDhS_expand[ugpr, ugpr, ugpr];
|
||||
@instantiate MapDQCOaLDhS_add[ugpr, ugpr, ugpr];
|
||||
@instantiate MapDQCOaLDhS_get[ugpr, ugpr, ugpr];
|
||||
|
||||
main: u0() -> {
|
||||
map.hash = &zero_hash;
|
||||
|
||||
ugpr test_key = 5;
|
||||
MapDQCOaLDhS_get[ugpr, ugpr, ugpr](&map, &test_key);
|
||||
MapDQCOaLDhS_add[ugpr, ugpr, ugpr](&map, &test_key, &test_value);
|
||||
MapDQCOaLDhS_get[ugpr, ugpr, ugpr](&map, &test_key);
|
||||
};
|
||||
|
||||
loop {}
|
||||
|
||||
@section(".data");
|
||||
MapDQCOaLDhS[ugpr, ugpr, ugpr; 32] map:;
|
||||
ugpr test_value: 10;
|
@ -92,7 +92,6 @@ void generic_visitor(AST **nptr, AST *stmt, AST *stmtPrev, AST *chu, AST *tlc, v
|
||||
generic_visitor(&n->stmtJump.condition, stmt, stmtPrev, chu, tlc, ud, preHandler, postHandler);
|
||||
}
|
||||
} else if(n->nodeKind == AST_STMT_LABEL) {
|
||||
} else if(n->nodeKind == AST_EXPR_NULL) {
|
||||
} else {
|
||||
stahp_node(n, "generic_visitor: unhandled %s", AST_KIND_STR[n->nodeKind]);
|
||||
}
|
||||
@ -286,55 +285,3 @@ AST *ast_cast_expr(AST *what, Type *to) {
|
||||
fail:
|
||||
stahp_node(what, "Cannot cast type %s into %s", type_to_string(what->expression.type), type_to_string(to));
|
||||
}
|
||||
|
||||
struct ReferencesStackState {
|
||||
bool yes;
|
||||
};
|
||||
static void references_stack_visitor(AST **nptr, AST *stmt, AST *stmtPrev, AST *chunk, AST *tlc, void *ud) {
|
||||
AST *n = *nptr;
|
||||
if(n->nodeKind == AST_EXPR_STACK_POINTER) {
|
||||
((struct ReferencesStackState*) ud)->yes = true;
|
||||
}
|
||||
}
|
||||
bool ast_references_stack(AST *a) {
|
||||
struct ReferencesStackState state = {};
|
||||
generic_visitor(&a, NULL, NULL, NULL, NULL, &state, references_stack_visitor, NULL);
|
||||
return state.yes;
|
||||
}
|
||||
|
||||
struct IsScopeItemReferenced {
|
||||
bool yes;
|
||||
ScopeItem *si;
|
||||
};
|
||||
static void is_scopeitem_referenced_visitor(AST **aptr, AST *stmt, AST *stmtPrev, AST *chunk, AST *tlc, void *ud) {
|
||||
struct IsScopeItemReferenced *state = ud;
|
||||
|
||||
AST *n = *aptr;
|
||||
if(n->nodeKind == AST_EXPR_UNARY_OP) {
|
||||
if(n->exprUnOp.operand->nodeKind == AST_EXPR_VAR && n->exprUnOp.operator == UNOP_REF && n->exprUnOp.operand->exprVar.thing == state->si) {
|
||||
state->yes = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
bool ast_is_scopeitem_referenced(AST *tlc, ScopeItem *si) {
|
||||
struct IsScopeItemReferenced state = {.si = si};
|
||||
generic_visitor(&tlc, NULL, NULL, tlc, tlc, &state, NULL, is_scopeitem_referenced_visitor);
|
||||
return state.yes;
|
||||
}
|
||||
|
||||
ScopeItem *ast_tlc_new_var(AST *tlc, char *name, Type *itstype) {
|
||||
ScopeItem *vte = calloc(1, sizeof(*vte));
|
||||
vte->kind = SCOPEITEM_VAR;
|
||||
vte->type = itstype;
|
||||
vte->data.var.color = -1;
|
||||
vte->data.var.precolored = false;
|
||||
vte->data.var.degree = 0;
|
||||
vte->data.var.priority = 0;
|
||||
vte->data.var.name = name;
|
||||
|
||||
// Add to var array
|
||||
tlc->chunk.vars = realloc(tlc->chunk.vars, sizeof(*tlc->chunk.vars) * (++tlc->chunk.varCount));
|
||||
tlc->chunk.vars[tlc->chunk.varCount - 1] = vte;
|
||||
|
||||
return vte;
|
||||
}
|
@ -44,8 +44,7 @@
|
||||
K(AST_EXPR_DOT) \
|
||||
K(AST_EXPR_EXT_SIZEOF) \
|
||||
K(AST_STMT_JUMP) \
|
||||
K(AST_STMT_LABEL) \
|
||||
K(AST_EXPR_NULL)
|
||||
K(AST_STMT_LABEL)
|
||||
|
||||
typedef enum ENUMPAK { AST_KINDS(GEN_ENUM) } ASTKind;
|
||||
extern const char *AST_KIND_STR[];
|
||||
@ -332,10 +331,6 @@ typedef struct {
|
||||
char *name;
|
||||
} ASTStmtLabel;
|
||||
|
||||
typedef struct {
|
||||
ASTExpr;
|
||||
} ASTExprNull;
|
||||
|
||||
typedef union AST {
|
||||
ASTBase;
|
||||
|
||||
@ -367,7 +362,6 @@ typedef union AST {
|
||||
ASTExprExtSizeOf exprExtSizeOf;
|
||||
ASTStmtJump stmtJump;
|
||||
ASTStmtLabel stmtLabel;
|
||||
ASTExprNull exprNull;
|
||||
} AST;
|
||||
|
||||
#pragma pack(pop)
|
||||
@ -387,13 +381,6 @@ void ast_typecheck(AST *tlc);
|
||||
|
||||
AST *ast_get_label_by_name(AST *tlc, const char *name);
|
||||
|
||||
bool ast_references_stack(AST *a);
|
||||
|
||||
bool ast_is_scopeitem_referenced(AST *tlc, ScopeItem *si);
|
||||
|
||||
// name becomes OWNED
|
||||
ScopeItem *ast_tlc_new_var(AST *tlc, char *name, Type *itstype);
|
||||
|
||||
#include"dump.h"
|
||||
#include"stack.h"
|
||||
#include"desegment.h"
|
||||
@ -401,6 +388,5 @@ ScopeItem *ast_tlc_new_var(AST *tlc, char *name, Type *itstype);
|
||||
#include"linearize.h"
|
||||
#include"usedef.h"
|
||||
#include"commutativity.h"
|
||||
#include"scr.h"
|
||||
|
||||
#endif
|
||||
|
@ -213,7 +213,18 @@ static char *ast_dumpe(AST *tlc, AST *e) {
|
||||
}
|
||||
|
||||
static char *ast_dumps(AST *tlc, AST *s) {
|
||||
if(s->nodeKind == AST_STMT_ASSIGN) {
|
||||
if(s->nodeKind == AST_STMT_DECL) {
|
||||
ScopeItem *vte = s->stmtDecl.thing;
|
||||
|
||||
if(vte->kind == SCOPEITEM_SYMBOL) {
|
||||
char *t = type_to_string(vte->type);
|
||||
char *e = s->stmtDecl.expression ? ast_dumpe(tlc, s->stmtDecl.expression) : strdup("");
|
||||
char *r = malp("%s%s %s: %s;", vte->data.symbol.isExternal ? "external " : "", t, vte->data.symbol.name, e);
|
||||
free(t);
|
||||
free(e);
|
||||
return r;
|
||||
}
|
||||
} else if(s->nodeKind == AST_STMT_ASSIGN) {
|
||||
if(s->stmtAssign.to) {
|
||||
char *a = ast_dumpe(tlc, s->stmtAssign.what);
|
||||
char *b = ast_dumpe(tlc, s->stmtAssign.to);
|
||||
@ -266,17 +277,14 @@ static char *ast_dumps(AST *tlc, AST *s) {
|
||||
} else if(s->nodeKind == AST_STMT_EXPR) {
|
||||
return ast_dumpe(tlc, s->stmtExpr.expr);
|
||||
} else if(s->nodeKind == AST_STMT_DECL) {
|
||||
ScopeItem *si = s->stmtDecl.thing;
|
||||
const char *name = si->kind == SCOPEITEM_VAR ? si->data.var.name : si->data.symbol.name;
|
||||
|
||||
char *a = type_to_string(si->type);
|
||||
char *a = type_to_string(s->stmtDecl.thing->type);
|
||||
char *c;
|
||||
if(s->stmtDecl.expression) {
|
||||
char *b = ast_dumpe(tlc, s->stmtDecl.expression);
|
||||
c = malp("%s %s = %s;", a, name, b);
|
||||
c = malp("%s %s = %s;", a, s->stmtDecl.thing->data.var.name, b);
|
||||
free(b);
|
||||
} else {
|
||||
c = malp("%s %s;", a, name);
|
||||
c = malp("%s %s;", a, s->stmtDecl.thing->data.var.name);
|
||||
}
|
||||
free(a);
|
||||
return c;
|
||||
|
@ -1,61 +0,0 @@
|
||||
#include"sroa.h"
|
||||
|
||||
#include"ast.h"
|
||||
#include"ntc.h"
|
||||
#include"utils.h"
|
||||
#include<stdlib.h>
|
||||
#include<assert.h>
|
||||
|
||||
static bool is_sroa_candidate(ScopeItem *si) {
|
||||
return si->type->type == TYPE_TYPE_RECORD && type_size(si->type) <= ntc_get_int_default("sroa-threshold", 32);
|
||||
}
|
||||
|
||||
struct SCRState {
|
||||
AST *tlc;
|
||||
};
|
||||
static void ast_scr_visitor(AST **aptr, AST *stmt, AST *stmtPrev, AST *chunk, AST *tlc, void *ud) {
|
||||
AST *n = *aptr;
|
||||
|
||||
struct SCRState *state = ud;
|
||||
|
||||
if(state->tlc != tlc) return;
|
||||
|
||||
if(n->nodeKind == AST_STMT_ASSIGN && n->stmtAssign.what->expression.type->type == TYPE_TYPE_RECORD) {
|
||||
Type *rectype = n->stmtAssign.what->expression.type;
|
||||
|
||||
for(size_t f = 0; f < rectype->record.fieldCount; f++) {
|
||||
ASTExprDot *dot1 = calloc(1, sizeof(*dot1));
|
||||
dot1->nodeKind = AST_EXPR_DOT;
|
||||
dot1->type = rectype->record.fieldTypes[f];
|
||||
dot1->a = ast_deep_copy(n->stmtAssign.what);
|
||||
dot1->b = strdup(rectype->record.fieldNames[f]);
|
||||
|
||||
ASTExprDot *dot2 = calloc(1, sizeof(*dot2));
|
||||
dot2->nodeKind = AST_EXPR_DOT;
|
||||
dot2->type = rectype->record.fieldTypes[f];
|
||||
dot2->a = ast_deep_copy(n->stmtAssign.to);
|
||||
dot2->b = strdup(rectype->record.fieldNames[f]);
|
||||
|
||||
ASTStmtAssign *assign = calloc(1, sizeof(*assign));
|
||||
assign->nodeKind = AST_STMT_ASSIGN;
|
||||
assign->what = (AST*) dot1;
|
||||
assign->to = (AST*) dot2;
|
||||
|
||||
stmtPrev->statement.next = (AST*) assign;
|
||||
stmtPrev = assign;
|
||||
}
|
||||
|
||||
stmtPrev->statement.next = stmt->statement.next;
|
||||
}
|
||||
}
|
||||
|
||||
void ast_secondclass_record(AST *tlc) {
|
||||
struct SCRState state = {.tlc = tlc};
|
||||
generic_visitor(&tlc, NULL, NULL, tlc, tlc, &state, ast_scr_visitor, NULL);
|
||||
|
||||
if(ntc_get_int("pdbg")) {
|
||||
char *astdump = ast_dump(tlc);
|
||||
fprintf(stderr, "### SCR ###\n%s\n", astdump);
|
||||
free(astdump);
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
// In machine code structs/records are second-class concepts.
|
||||
// Either the record gets split into its separate fields (SRoA),
|
||||
// or it must be spilled into memory.
|
||||
//
|
||||
// In any case, many QoL features programmers love (assigning structs to
|
||||
// structs or passing structs as arguments to functions) don't exist in
|
||||
// machine code and must be converted to a valid but equivalent form.
|
||||
|
||||
union AST;
|
||||
void ast_secondclass_record(union AST *tlc);
|
@ -10,6 +10,26 @@ static bool is_sroa_candidate(ScopeItem *si) {
|
||||
return si->type->type == TYPE_TYPE_RECORD && type_size(si->type) <= ntc_get_int_default("sroa-threshold", 32);
|
||||
}
|
||||
|
||||
struct IsScopeItemReferenced {
|
||||
bool yes;
|
||||
ScopeItem *si;
|
||||
};
|
||||
static void is_scopeitem_referenced_visitor(AST **aptr, AST *stmt, AST *stmtPrev, AST *chunk, AST *tlc, void *ud) {
|
||||
struct IsScopeItemReferenced *state = ud;
|
||||
|
||||
AST *n = *aptr;
|
||||
if(n->nodeKind == AST_EXPR_UNARY_OP) {
|
||||
if(n->exprUnOp.operand->nodeKind == AST_EXPR_VAR && n->exprUnOp.operator == UNOP_REF && n->exprUnOp.operand->exprVar.thing == state->si) {
|
||||
state->yes = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
static bool is_scopeitem_referenced(AST *tlc, ScopeItem *si) {
|
||||
struct IsScopeItemReferenced state = {.si = si};
|
||||
generic_visitor(&tlc, NULL, NULL, tlc, tlc, &state, NULL, is_scopeitem_referenced_visitor);
|
||||
return state.yes;
|
||||
}
|
||||
|
||||
struct DecomposeAutomaticRecordState {
|
||||
AST *tlc;
|
||||
ScopeItem *target;
|
||||
@ -69,7 +89,7 @@ void ast_sroa(AST *tlc) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if(ast_is_scopeitem_referenced(tlc, si)) {
|
||||
if(is_scopeitem_referenced(tlc, si)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -81,10 +101,4 @@ void ast_sroa(AST *tlc) {
|
||||
/* Restart */
|
||||
i = -1;
|
||||
}
|
||||
|
||||
if(ntc_get_int("pdbg")) {
|
||||
char *astdump = ast_dump(tlc);
|
||||
fprintf(stderr, "### SROA ###\n%s\n", astdump);
|
||||
free(astdump);
|
||||
}
|
||||
}
|
||||
|
@ -57,7 +57,7 @@ static void spill2stack_visitor(AST **aptr, AST *stmt, AST *stmtPrev, AST *chunk
|
||||
|
||||
} else if(a->nodeKind == AST_EXPR_PRIMITIVE && a->exprPrim.stackGrowth) {
|
||||
|
||||
// Guaranteed to not require more normalization
|
||||
// Guaranteed to not require more dumbification
|
||||
a->exprPrim.val += this->stackGrowth;
|
||||
|
||||
}
|
||||
|
@ -190,10 +190,8 @@ void ast_usedef_reset(AST *chu) {
|
||||
|
||||
assert(!!vte->data.var.usedefFirst == !!vte->data.var.usedefLast);
|
||||
|
||||
if(vte->data.var.usedefFirst) {
|
||||
vte->data.var.liveRangeStart = vte->data.var.usedefFirst->stmt;
|
||||
vte->data.var.liveRangeEnd = vte->data.var.usedefLast->stmt;
|
||||
}
|
||||
vte->data.var.liveRangeStart = vte->data.var.usedefFirst->stmt;
|
||||
vte->data.var.liveRangeEnd = vte->data.var.usedefLast->stmt;
|
||||
}
|
||||
|
||||
// fix liveRangeStart and/or liveRangeEnd depending on goto targets
|
||||
|
@ -27,9 +27,7 @@ char *TOKEN_NAMES[] = {
|
||||
"'break'",
|
||||
"','",
|
||||
"'&'",
|
||||
"'&&'",
|
||||
"'|'",
|
||||
"'||'",
|
||||
"'^'",
|
||||
"'~'",
|
||||
"'=='",
|
||||
@ -52,7 +50,6 @@ char *TOKEN_NAMES[] = {
|
||||
"'as'",
|
||||
"'use'",
|
||||
"'else'",
|
||||
"'null'",
|
||||
};
|
||||
|
||||
static int isAlpha(int c) {
|
||||
@ -312,10 +309,6 @@ Token nct_tokenize(FILE *f) {
|
||||
free(content);
|
||||
tok.type = TOKEN_ELSE;
|
||||
return tok;
|
||||
} else if(!strcmp(content, "null")) {
|
||||
free(content);
|
||||
tok.type = TOKEN_NULL;
|
||||
return tok;
|
||||
}
|
||||
|
||||
tok.type = TOKEN_IDENTIFIER;
|
||||
|
@ -52,7 +52,6 @@ typedef enum {
|
||||
TOKEN_AS,
|
||||
TOKEN_USE,
|
||||
TOKEN_ELSE,
|
||||
TOKEN_NULL,
|
||||
} TokenKind;
|
||||
|
||||
typedef struct Token {
|
||||
|
@ -97,16 +97,15 @@ int main(int argc_, char **argv_) {
|
||||
}
|
||||
|
||||
ast_segmented_dereference(chunk);
|
||||
ast_secondclass_record(chunk);
|
||||
ast_sroa(chunk);
|
||||
|
||||
ast_linearize(chunk);
|
||||
|
||||
arch_normalize_pre(chunk);
|
||||
dumben_pre(chunk);
|
||||
|
||||
arch_normalize(chunk);
|
||||
dumben_go(chunk);
|
||||
while(!cg_go(chunk)) {
|
||||
arch_normalize(chunk);
|
||||
dumben_go(chunk);
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
@ -13,11 +13,10 @@ intmax_t ntc_get_int(const char *name);
|
||||
intmax_t ntc_get_int_default(const char *name, intmax_t def);
|
||||
|
||||
union AST;
|
||||
void arch_init();
|
||||
bool arch_verify_target();
|
||||
int arch_ptr_size();
|
||||
void arch_normalize_pre(union AST *tlc);
|
||||
void arch_normalize(union AST *tlc);
|
||||
void dumben_pre(union AST *tlc);
|
||||
void dumben_go(union AST *tlc);
|
||||
int cg_go(union AST *tlc);
|
||||
|
||||
#endif
|
||||
|
@ -269,12 +269,6 @@ AST *nct_parse_expression(Parser *P, int lOP) {
|
||||
|
||||
e = (AST*) exprvar(P, vte);
|
||||
}
|
||||
} else if(maybe(P, TOKEN_NULL)) {
|
||||
ASTExprNull *ret = alloc_node(P, sizeof(*ret));
|
||||
ret->nodeKind = AST_EXPR_NULL;
|
||||
ret->type = type_pointer_wrap(type_u(8));
|
||||
|
||||
e = (AST*) ret;
|
||||
} else if(peek(P, 0).type == TOKEN_STRING) {
|
||||
ASTExprStringLiteral *ret = alloc_node(P, sizeof(*ret));
|
||||
ret->nodeKind = AST_EXPR_STRING_LITERAL;
|
||||
@ -296,7 +290,7 @@ AST *nct_parse_expression(Parser *P, int lOP) {
|
||||
Token op = P->tokens[P->i - 1];
|
||||
|
||||
if(op.type == TOKEN_DOT) {
|
||||
while(e->expression.type->type == TYPE_TYPE_POINTER) {
|
||||
while(type_is_any_pointer(e->expression.type)) {
|
||||
AST *deref = alloc_node(P, sizeof(ASTExprUnaryOp));
|
||||
deref->nodeKind = AST_EXPR_UNARY_OP;
|
||||
deref->exprUnOp.operator = UNOP_DEREF;
|
||||
|
@ -207,7 +207,7 @@ int type_is_castable(Type *from, Type *to) {
|
||||
}
|
||||
}
|
||||
|
||||
if(from->type == TYPE_TYPE_PRIMITIVE && to->type == TYPE_TYPE_POINTER || from->type == TYPE_TYPE_POINTER && to->type == TYPE_TYPE_PRIMITIVE) {
|
||||
if(from->type == TYPE_TYPE_PRIMITIVE && to->type == TYPE_TYPE_POINTER) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
|
@ -144,9 +144,7 @@ static inline int is_xop(AST *e) {
|
||||
c = c->exprCast.what;
|
||||
}
|
||||
|
||||
if(c->nodeKind == AST_EXPR_STACK_POINTER) {
|
||||
return XOP_MEM;
|
||||
} else if(c->nodeKind == AST_EXPR_VAR && c->exprVar.thing->kind == SCOPEITEM_VAR) {
|
||||
if(c->nodeKind == AST_EXPR_VAR && c->exprVar.thing->kind == SCOPEITEM_VAR) {
|
||||
if(x86_ia16() && !x86_is_marked_ptr(c->exprVar.thing)) {
|
||||
// In IA-16, pointers MUST be preclassed to REG_CLASS_IA16_PTRS
|
||||
return XOP_NOT_XOP;
|
||||
|
12
src/x86/cg.c
12
src/x86/cg.c
@ -195,12 +195,6 @@ static const char *xop_sz(AST *tlc, AST *e, int sz) {
|
||||
xv_sz(p->exprBinOp.operands[1]->exprBinOp.operands[1]->exprVar.thing, 0));
|
||||
} else if(p->nodeKind == AST_EXPR_VAR && p->exprVar.thing->kind == SCOPEITEM_VAR) {
|
||||
pr = snprintf(ret, XOPBUFSZ, "%s [%s]", spec(sz), xv_sz(p->exprVar.thing, 0));
|
||||
} else if(p->nodeKind == AST_EXPR_STACK_POINTER) {
|
||||
if(x86_ia16()) {
|
||||
pr = snprintf(ret, XOPBUFSZ, "[bp + %li]", 0 - tlc->chunk.stackReservation);
|
||||
} else {
|
||||
pr = snprintf(ret, XOPBUFSZ, "[esp + %i]", 0);
|
||||
}
|
||||
} else if(p->nodeKind == AST_EXPR_BINARY_OP && p->exprBinOp.operator == BINOP_ADD && p->exprBinOp.operands[0]->nodeKind == AST_EXPR_STACK_POINTER && p->exprBinOp.operands[1]->nodeKind == AST_EXPR_PRIMITIVE) {
|
||||
if(x86_ia16()) {
|
||||
pr = snprintf(ret, XOPBUFSZ, "[bp + %li]", p->exprBinOp.operands[1]->exprPrim.val - tlc->chunk.stackReservation);
|
||||
@ -364,11 +358,11 @@ void cg_chunk(CGState *cg, AST *a) {
|
||||
|
||||
ast_linearize(s->stmtDecl.expression->exprFunc.chunk);
|
||||
|
||||
arch_normalize_pre(s->stmtDecl.expression->exprFunc.chunk);
|
||||
dumben_pre(s->stmtDecl.expression->exprFunc.chunk);
|
||||
|
||||
arch_normalize(s->stmtDecl.expression->exprFunc.chunk);
|
||||
dumben_go(s->stmtDecl.expression->exprFunc.chunk);
|
||||
while(!cg_go(s->stmtDecl.expression->exprFunc.chunk)) {
|
||||
arch_normalize(s->stmtDecl.expression->exprFunc.chunk);
|
||||
dumben_go(s->stmtDecl.expression->exprFunc.chunk);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -6,20 +6,36 @@
|
||||
#include"reporting.h"
|
||||
#include"utils.h"
|
||||
|
||||
// This is the normalization pass:
|
||||
// Complex expressions are to be "broken down" into simpler ones until
|
||||
// the AST can be trivially translated to the target architecture.
|
||||
// This file along with CG is strictly for IA-32 & IA-16 and will fail
|
||||
// for other architectures.
|
||||
// This is the dumbing down pass.
|
||||
//
|
||||
// Complex expressions are to be "broken down" into simpler ones until the AST
|
||||
// can be trivially translated to the target architecture.
|
||||
//
|
||||
// This file along with CG is strictly for IA-32 and will fail for other
|
||||
// architectures.
|
||||
|
||||
static ScopeItem *create_temp(AST *tlc, Type *itstype) {
|
||||
static ScopeItem *create_dumbtemp(AST *tlc, Type *itstype) {
|
||||
static size_t vidx = 0;
|
||||
return ast_tlc_new_var(tlc, malp("nrm%lu", vidx++), itstype);
|
||||
|
||||
ScopeItem *vte = calloc(1, sizeof(*vte));
|
||||
vte->kind = SCOPEITEM_VAR;
|
||||
vte->type = itstype;
|
||||
vte->data.var.color = -1;
|
||||
vte->data.var.precolored = false;
|
||||
vte->data.var.degree = 0;
|
||||
vte->data.var.priority = 0;
|
||||
vte->data.var.name = malp("$dumb%lu", vidx++);
|
||||
|
||||
// Add to var array
|
||||
tlc->chunk.vars = realloc(tlc->chunk.vars, sizeof(*tlc->chunk.vars) * (++tlc->chunk.varCount));
|
||||
tlc->chunk.vars[tlc->chunk.varCount - 1] = vte;
|
||||
|
||||
return vte;
|
||||
}
|
||||
|
||||
/* Split away complex expression into a new local variable */
|
||||
static AST *varify(AST *tlc, AST *chunk, AST *stmtPrev, AST *stmt, AST *e) {
|
||||
ScopeItem *vte = create_temp(tlc, e->expression.type);
|
||||
ScopeItem *vte = create_dumbtemp(tlc, e->expression.type);
|
||||
|
||||
// Alter AST
|
||||
|
||||
@ -55,8 +71,6 @@ static AST *xopify(AST *tlc, AST *chunk, AST *stmtPrev, AST *stmt, AST *e) {
|
||||
return varify(tlc, chunk, stmtPrev, stmt, e);
|
||||
}
|
||||
|
||||
// This is needed for architectures where not all registers can be used
|
||||
// as pointers (for example IA-16 or the 68000 series)
|
||||
static void mark_ptr(AST *a) {
|
||||
assert(a->nodeKind == AST_EXPR_VAR);
|
||||
assert(a->exprVar.thing->kind == SCOPEITEM_VAR);
|
||||
@ -99,12 +113,12 @@ static void mark_d(ScopeItem *si) {
|
||||
* RULE ONE OF DUMBING: NEVER xopify NOR varify MORE THAN ONCE IN A SINGLE CALL TO VISITOR!!!
|
||||
* IF YOU DO THIS, stmtPrev WILL FUCK UP AND STATEMENTS WILL BE LOST
|
||||
*/
|
||||
struct NormState {
|
||||
struct DumbenState {
|
||||
AST *targetTLC;
|
||||
int effective;
|
||||
};
|
||||
static void normalize_visitor(AST **nptr, AST *stmt, AST *stmtPrev, AST *chu, AST *tlc, void *ud) {
|
||||
struct NormState *this = ud;
|
||||
static void dumben_visitor(AST **nptr, AST *stmt, AST *stmtPrev, AST *chu, AST *tlc, void *ud) {
|
||||
struct DumbenState *this = ud;
|
||||
|
||||
if(this->targetTLC != tlc) return;
|
||||
|
||||
@ -284,7 +298,7 @@ static void normalize_visitor(AST **nptr, AST *stmt, AST *stmtPrev, AST *chu, AS
|
||||
|
||||
if(s->stmtAssign.to->nodeKind == AST_EXPR_CALL && (s->stmtAssign.what->nodeKind != AST_EXPR_VAR || s->stmtAssign.what->exprVar.thing->kind != SCOPEITEM_VAR || !s->stmtAssign.what->exprVar.thing->data.var.precolored)) {
|
||||
|
||||
ScopeItem *tmp = create_temp(tlc, s->stmtAssign.what->expression.type);
|
||||
ScopeItem *tmp = create_dumbtemp(tlc, s->stmtAssign.what->expression.type);
|
||||
mark_a(tmp);
|
||||
|
||||
ASTExprVar *ev[2] = {calloc(1, sizeof(**ev)), calloc(1, sizeof(**ev))};
|
||||
@ -332,9 +346,7 @@ static void normalize_visitor(AST **nptr, AST *stmt, AST *stmtPrev, AST *chu, AS
|
||||
int argCount = call->what->expression.type->pointer.of->function.argCount;
|
||||
|
||||
for(int i = 0; i < argCount; i++) {
|
||||
// We also check for ast_references_stack because the stack gets
|
||||
// shifted during a function call which screws up all of the offsets
|
||||
if(is_xop(call->args[i]) == XOP_NOT_XOP || ast_references_stack(call->args[i])) {
|
||||
if(is_xop(call->args[i]) == XOP_NOT_XOP) {
|
||||
call->args[i] = xopify(tlc, chu, stmtPrev, s, call->args[i]);
|
||||
this->effective = 1;
|
||||
|
||||
@ -472,7 +484,7 @@ static void normalize_visitor(AST **nptr, AST *stmt, AST *stmtPrev, AST *chu, AS
|
||||
}
|
||||
}
|
||||
|
||||
static void pre_norm_visitor(AST **nptr, AST *stmt, AST *stmtPrev, AST *chunk, AST *tlc, void *ud) {
|
||||
static void pre_dumb_visitor(AST **nptr, AST *stmt, AST *stmtPrev, AST *chunk, AST *tlc, void *ud) {
|
||||
AST *n = *nptr;
|
||||
|
||||
if(n == ud) {
|
||||
@ -590,22 +602,13 @@ struct DenoopState {
|
||||
static void denoop_visitor(AST **nptr, AST *stmt, AST *stmtPrev, AST *chunk, AST *tlc, void *ud) {
|
||||
struct DenoopState *state = ud;
|
||||
|
||||
if(state->targetTLC != tlc) return;
|
||||
// if(state->targetTLC != tlc) return;
|
||||
|
||||
AST *n = *nptr;
|
||||
|
||||
bool *success = &state->success;
|
||||
|
||||
if(n->nodeKind == AST_EXPR_NULL) {
|
||||
// Turn `null` into integer
|
||||
|
||||
ASTExprPrimitive *prim = calloc(1, sizeof(*prim));
|
||||
prim->nodeKind = AST_EXPR_PRIMITIVE;
|
||||
prim->type = n->expression.type;
|
||||
prim->val = ntc_get_int_default("null", 0);
|
||||
|
||||
*nptr = (AST*) prim;
|
||||
} else if(n->nodeKind == AST_EXPR_UNARY_OP && n->exprUnOp.operator == UNOP_REF && n->exprUnOp.operand->nodeKind == AST_EXPR_UNARY_OP && n->exprUnOp.operand->exprUnOp.operator == UNOP_DEREF) {
|
||||
if(n->nodeKind == AST_EXPR_UNARY_OP && n->exprUnOp.operator == UNOP_REF && n->exprUnOp.operand->nodeKind == AST_EXPR_UNARY_OP && n->exprUnOp.operand->exprUnOp.operator == UNOP_DEREF) {
|
||||
// Turn `&*a` into `a`
|
||||
|
||||
// Artificially change type of casted expression to keep types valid for subsequent passes
|
||||
@ -628,16 +631,15 @@ static void denoop_visitor(AST **nptr, AST *stmt, AST *stmtPrev, AST *chunk, AST
|
||||
n->exprCast.what = n->exprCast.what->exprCast.what;
|
||||
|
||||
*success = true;
|
||||
} else if(n->nodeKind == AST_EXPR_BINARY_OP && n->exprBinOp.operands[0]->nodeKind == AST_EXPR_BINARY_OP && n->exprBinOp.operator == BINOP_ADD && n->exprBinOp.operands[0]->exprBinOp.operator == BINOP_ADD && n->exprBinOp.operands[1]->nodeKind == AST_EXPR_PRIMITIVE && n->exprBinOp.operands[0]->exprBinOp.operands[1]->nodeKind == AST_EXPR_PRIMITIVE && !(n->exprBinOp.operands[0]->exprBinOp.operands[1]->exprPrim.stackGrowth && n->exprBinOp.operands[1]->exprPrim.stackGrowth)) {
|
||||
} else if(n->nodeKind == AST_EXPR_BINARY_OP && n->exprBinOp.operands[0]->nodeKind == AST_EXPR_BINARY_OP && n->exprBinOp.operator == BINOP_ADD && n->exprBinOp.operands[0]->exprBinOp.operator == BINOP_ADD && n->exprBinOp.operands[1]->nodeKind == AST_EXPR_PRIMITIVE && n->exprBinOp.operands[0]->exprBinOp.operands[1]->nodeKind == AST_EXPR_PRIMITIVE) {
|
||||
// Turn `(x + a) + b` into `x + (a + b)`
|
||||
|
||||
n->exprBinOp.operands[0]->exprBinOp.operands[1]->exprPrim.val += n->exprBinOp.operands[1]->exprPrim.val;
|
||||
n->exprBinOp.operands[0]->exprBinOp.operands[1]->exprPrim.stackGrowth = n->exprBinOp.operands[0]->exprBinOp.operands[1]->exprPrim.stackGrowth || n->exprBinOp.operands[1]->exprPrim.stackGrowth;
|
||||
|
||||
*nptr = n->exprBinOp.operands[0];
|
||||
|
||||
*success = true;
|
||||
} else if(n->nodeKind == AST_EXPR_BINARY_OP && n->exprBinOp.operator == BINOP_ADD && n->exprBinOp.operands[1]->nodeKind == AST_EXPR_PRIMITIVE && n->exprBinOp.operands[1]->exprPrim.val == 0 && !n->exprBinOp.operands[1]->exprPrim.stackGrowth) {
|
||||
} else if(n->nodeKind == AST_EXPR_BINARY_OP && n->exprBinOp.operator == BINOP_ADD && n->exprBinOp.operands[1]->nodeKind == AST_EXPR_PRIMITIVE && n->exprBinOp.operands[1]->exprPrim.val == 0) {
|
||||
// Turn `x + 0` into `x`
|
||||
|
||||
// Artificially change type of casted expression to keep types valid for subsequent passes
|
||||
@ -669,24 +671,22 @@ static void denoop_visitor(AST **nptr, AST *stmt, AST *stmtPrev, AST *chunk, AST
|
||||
*nptr = n->exprCast.what;
|
||||
|
||||
*success = true;
|
||||
} else if(n->nodeKind == AST_EXPR_BINARY_OP && n->exprBinOp.operator == BINOP_ADD && n->exprBinOp.operands[0]->nodeKind == AST_EXPR_PRIMITIVE && n->exprBinOp.operands[1]->nodeKind == AST_EXPR_PRIMITIVE && !(n->exprBinOp.operands[0]->exprPrim.stackGrowth && n->exprBinOp.operands[1]->exprPrim.stackGrowth)) {
|
||||
} else if(n->nodeKind == AST_EXPR_BINARY_OP && n->exprBinOp.operator == BINOP_ADD && n->exprBinOp.operands[0]->nodeKind == AST_EXPR_PRIMITIVE && n->exprBinOp.operands[1]->nodeKind == AST_EXPR_PRIMITIVE) {
|
||||
// Constant propagation of + operator
|
||||
|
||||
AST *prim = n->exprBinOp.operands[0];
|
||||
prim->expression.type = n->exprBinOp.type;
|
||||
prim->exprPrim.val = n->exprBinOp.operands[0]->exprPrim.val + n->exprBinOp.operands[1]->exprPrim.val;
|
||||
prim->exprPrim.stackGrowth = n->exprBinOp.operands[0]->exprPrim.stackGrowth || n->exprBinOp.operands[1]->exprPrim.stackGrowth;
|
||||
|
||||
*nptr = prim;
|
||||
|
||||
*success = true;
|
||||
} else if(n->nodeKind == AST_EXPR_BINARY_OP && n->exprBinOp.operator == BINOP_SUB && n->exprBinOp.operands[0]->nodeKind == AST_EXPR_PRIMITIVE && n->exprBinOp.operands[1]->nodeKind == AST_EXPR_PRIMITIVE && !(n->exprBinOp.operands[0]->exprPrim.stackGrowth && n->exprBinOp.operands[1]->exprPrim.stackGrowth)) {
|
||||
} else if(n->nodeKind == AST_EXPR_BINARY_OP && n->exprBinOp.operator == BINOP_SUB && n->exprBinOp.operands[0]->nodeKind == AST_EXPR_PRIMITIVE && n->exprBinOp.operands[1]->nodeKind == AST_EXPR_PRIMITIVE) {
|
||||
// Constant propagation of - operator
|
||||
|
||||
AST *prim = n->exprBinOp.operands[0];
|
||||
prim->expression.type = n->exprBinOp.type;
|
||||
prim->exprPrim.val = n->exprBinOp.operands[0]->exprPrim.val - n->exprBinOp.operands[1]->exprPrim.val;
|
||||
prim->exprPrim.stackGrowth = n->exprBinOp.operands[0]->exprPrim.stackGrowth || n->exprBinOp.operands[1]->exprPrim.stackGrowth;
|
||||
|
||||
*nptr = prim;
|
||||
|
||||
@ -775,13 +775,13 @@ static void convention_correctness_visitor(AST **nptr, AST *stmt, AST *stmtPrev,
|
||||
}
|
||||
}
|
||||
|
||||
void arch_normalize_pre(AST *tlc) {
|
||||
void dumben_pre(AST *tlc) {
|
||||
generic_visitor(&tlc, NULL, NULL, tlc, tlc, tlc, convention_correctness_visitor, NULL);
|
||||
generic_visitor(&tlc, NULL, NULL, tlc, tlc, tlc, pre_norm_visitor, NULL);
|
||||
generic_visitor(&tlc, NULL, NULL, tlc, tlc, tlc, pre_dumb_visitor, NULL);
|
||||
generic_visitor(&tlc, NULL, NULL, tlc, tlc, tlc, decompose_symbol_record_field_access, NULL);
|
||||
|
||||
for(size_t t = 0; t < tlc->chunk.varCount;) {
|
||||
if(ast_is_scopeitem_referenced(tlc, tlc->chunk.vars[t]) || tlc->chunk.vars[t]->type->type == TYPE_TYPE_RECORD) {
|
||||
if(tlc->chunk.vars[t]->type->type == TYPE_TYPE_RECORD) {
|
||||
ast_spill_to_stack(tlc, tlc->chunk.vars[t]);
|
||||
} else {
|
||||
t++;
|
||||
@ -793,16 +793,16 @@ void arch_normalize_pre(AST *tlc) {
|
||||
ast_commutativity_pass(tlc);
|
||||
}
|
||||
|
||||
void arch_normalize(AST* tlc) {
|
||||
void dumben_go(AST* tlc) {
|
||||
size_t i = 0;
|
||||
while(1) {
|
||||
if(i == 20000) {
|
||||
stahp(0, 0, "TOO MANY DUMBS. TOO MANY DUMBS.");
|
||||
}
|
||||
|
||||
struct NormState state = {.targetTLC = tlc};
|
||||
struct DumbenState state = {.targetTLC = tlc};
|
||||
|
||||
generic_visitor(&tlc, NULL, NULL, tlc, tlc, &state, normalize_visitor, NULL);
|
||||
generic_visitor(&tlc, NULL, NULL, tlc, tlc, &state, dumben_visitor, NULL);
|
||||
|
||||
int successful = state.effective;
|
||||
|
||||
@ -811,7 +811,7 @@ void arch_normalize(AST* tlc) {
|
||||
}
|
||||
|
||||
if(ntc_get_int("pdbg")) {
|
||||
fprintf(stderr, "### NORM %lu ###\n", i++);
|
||||
fprintf(stderr, "### DUMBED DOWN %lu ###\n", i++);
|
||||
char *astdump = ast_dump(tlc);
|
||||
fputs(astdump, stderr);
|
||||
free(astdump);
|
Loading…
Reference in New Issue
Block a user