# Expression code generation The code at [comp/c/expr.fs] could be called "where the real deal is". It takes an AST [comp/c/ast], which describes an expression, and recursively generates code for it. This unit is at the top of the pyramid and heavily rests on these underlying concepts: * [hal] * [lib/type] * [lib/struct] * [comp/sig] * [comp/sym] * [comp/reg] An understanding on this unit requires an understanding of those above. The main word of this unit, "expr, ( ast -- halop )" takes an AST, recursively generates code for it, then yields a HAL operand [hal]. It's turtles all the way down, "expr," recursively calls itself until it meets a simple node such as "lit" or "sym". ## Symbols Symbols, being a "bottom" node, are pretty straightforward. They transform a name into a type/halop pair. This is done in "getsym". First, we look into the local namespace (arguments and local variables) through [comp/sym]. If there's no match, we look for a sysdict entry, then check for an entry tag. If found, and if it's a CNST or VALU [doc/usage/tag], we have a regular uint immediate or memory reference. Otherwise, we check if the execution token associated with the entry has a SIGT [comp/sig] associated to it [lib/tagl]. If it does, then our entry is a callable function. Otherwise, symbol not found. ## binary operations Generating code for unary operations is very straightforward. You just recursively apply the child AST, do your stuff, yield the result in W. Very easy. However, most of the "meat", complexity-wise, is around codegen for binary operators such as + << -= etc. Those binary operators are arbitrarily complex and require us to possibly store intermediate values somewhere. That somewhere is the [comp/reg] mechanism. In our general logic, we always resolve the righthand of a binop before its lefthand. We could do the opposite, but we have to choose a consistent order on which we can rely to simplify the code. Because of the way much of the C code looks like (for example "a = b + c"), the righthand often is "heavier" and codegen is more efficient if we being with it, so we do. Once we've generated that righthand, we check its halop. If it's NOT a W) halop, it means that it's a "no computing" AST, for example a symbol or constant. This means we don't have anything to worry about, no extra register needed, just do the thing. However, if the righthand's result IS a W), then we need to save that result to a register before we compute the lefthand, which we do through the [comp/reg] mechanism. ## boolexpr, Naively resolving boolean expression ( == != <= etc.) by yielding a 0 or 1 to the W register works, but its very inefficient. Example: if (a == b) { ... } If you naively generate "a == b", which *needs* to be able to yield a 0 or 1 in case it's used in "return a == b", then you end up with a HAL equivalent that might look like: RSP) @, RSP) 4 +) =) bool, \ generated by the "==" 0 i) <>) if, \ generated by the "if" It's correct, but it could have been: RSP) @, RSP) 4 +) =) if, And this is why we have "boolexpr, ( ast -- halop cond 0 OR f 1 )". This word can be called instead of "expr," in cases where we branch on the result of the expression, that is, under "if", "while" and "for". All in all, what boolexpr, does is that it omits the very last part of the expression resolving and yields a result that can easily be plugged into "bool," or "if,". To see it, you can look at "boolop", the "regular" handler for boolean operators, and see that it's basically only "boolexpr, bool,", with special handling for the case where we have a constant expression. ## Function calls In principle, function calling is straightforward, but it's the little details that makes it tricky. So, in principle, the process is to: 1. Check for signature compatibility 2. Do codegen on all argument ASTs, each time pushing the result to PS 3. The last argument stays in W 4. Generate a brr, And that's pretty much it! But the details, oh the details! See below. ### Signature checking The word "type=" from [comp/c/type] is fancy enough to compare signatures and ensuring that those signatures are compatible, even printing a pretty helpful error message when they're not. We can delegate that complexity to it. However, our input funcall AST doesn't supply a signature for its arguments, only a list of ASTs. We have to create a temporary fake signature from these AST. Because we don't check the return value of our call (it's checked higher up), we unconditionally place the return value from the target signature in there. Then we're ready to call type=. That's it! ### Saving registers If the funcall happens in the middle of a complex expression, it's likely that registers containing intermediate results are in use. When they are, we must save them before the "brr," and restore them after it. ### PS displacement As we place arguments on PS, we displace the symbols living in PS, possibly making further expr codegen refer to the wrong PS slot. Therefore, we adjust [comp/sym] PS displacement as we go and take care of restoring it afterwards. ### Call address on W When we call a function that lives on W, that is, when the address is the result of some runtime work, we have a problem because W won't be valid anymore after arguments are placed. We use [comp/reg] to save that address, but we do so *after* our pushregs, call because we don't need to save that value because as soon as brr, is called, it can be forgot. ### Return value PS fiddling When we're in a C function codegen, we always keep W free. However, that's not Dusk's semantics. When we perform a function call, if that function has a return value, it lives in W. This is *good*, this is what we want and there's nothing to do. However, if that function has a void return value, it yield *nothing*. This means that after the call, our PS has slided by one slot. We need to re-establish PS balance with a "dup,". Inversely, if we call a function that takes no argument, our W register kind of magically ends up in PS. If the called word has a ( -- ), no harm, no foul because nothing has been changed, so there's nothing to do. However, if the called word has a return value, for example ( -- int ), then we have a problem: The result is in W, fine, but whatever was in W before is now in PS, sliding it out the other side. We need a "nip," to restore balance. ## Typecasts Typecasts are straightforward: resolve a single expression, then forcefully replace the yielded type with whatever was specified as the type to cast. There are caveats. Because all arithmetics generated by the HAL are 32-bit, when we shrink a type (for example going from int to char), we also need to mask the value, otherwise our typecast will be ineffective. Also, when enlarging a type to a signed type (regardless of whether the previous type was signed), we need to sign-extend th value. Regardless of whether we mask or sign-extend, when size has changed, we also need to forcefully bring the value to W. If we keep it as a memory reference, we might over-read our final value or, under big endian, read an entirely wrong value. So that's most of the fiddling in the typecast handler. ## API expr, ( ast -- halop ) Recursively resolve the AST node [comp/c/ast], generate the appropriate code corresponding to it using the HAL [doc/hal], then yields the HAL operand describing where the resulting value lives after the generated code has run. boolexpr, ( ast -- halop cond 0 OR f 1 ) Assume "ast" is one of the boolean expressions, generate code for it like "expr," would, but omit the final "bool," HAL call which would yield a 0 or 1 in the W register. Instead, yield the arguments you would pass to "bool,". Those arguments can be passed to "if," instead. When expression resolution yields a constant, yield f=boolvalue instead. Example from "boolop": boolexpr, if i) else bool, W) &) then effect, ( ast -- ) Same as expr, but don't care about the result. In other words, only compile the side effect of an expression. This implicitly clears W. You'll want to use this, for example, in function body non-return statements and in the first and third components of a for loop. This allows for some optimization, notably in postinc, postdec and assign.