# 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.
