This commit reimplements commit 6515 to happen during type-checking rather
than as early as possible. That way we naturally get a more informative
error message.
The new failing test is now passing, and so is this manual test that had
been throwing a spurious error:
fn foo {
var a/eax: int <- copy 0
var b/ebx: int <- copy 0
{
var a1/eax: int <- copy 0
var b1/ebx: int <- copy a1
}
b <- copy a
}
However, factorial.mu is still throwing a spurious error.
Some history on this commit's fix: When I moved stack-location tracking
out of the parsing phase (commit 6116, Mar 10) I thoughtlessly moved block-depth
tracking as well. And the reason that happened: I'd somehow gotten by without
ever cleaning up vars from a block during parsing. For all my tests, this
is a troubling sign that I'm not testing enough.
The good news: clean-up-blocks works perfectly during parsing.
Before: bytes can't live on the stack, so size(byte) == 1 just for array
elements.
After: bytes mostly can't live on the stack except for function args (which
seem too useful to disallow), so size(byte) == 4 except there's now a new
primitive called element-size for array elements where size(byte) == 1.
Now apps/browse.subx starts working again.
Several bugs fixed in the process, and expectation of further bugs is growing.
I'd somehow started assuming I don't need to have separate cases for rm32
as a register vs mem. That's not right. We might need more reg-reg Primitives.
Most unbelievably, I'd forgotten to pass the output 'out' arg to 'lookup-var'
long before the recent additions of 'err' and 'ed' args. But things continued
to work because an earlier call just happened to leave the arg at just
the right place on the stack. So we only caught all these places when we
had to provide error messages.
Byte-oriented addressing is only supported in a couple of instructions
in SubX. As a result, variables of type 'byte' can't live on the stack,
or in registers 'esi' and 'edi'.
I had a little "optimization" to avoid creating nested blocks if "they weren't
needed". Except, of course, they were. Lose the optimization. Sometimes
we create multiple jumps when a single one would suffice. Ignore that for
now.
The rule: emit spills for a register unless the output is written somewhere
in the current block after the current instruction. Including in nested
blocks.
Let's see if this is right.
Rather than have two ways to decide whether to emit push/pop instructions,
just record for each var on the 'vars' stack whether we emitted a push
for it, and reuse the decision to emit a pop.
Observations:
- the orchestration from 'in' to 'addr-in' to '_in-addr' to 'in-addr'
is quite painful. Once to turn a handle into its address, once to turn
a handle into the address of its payload, and a third time to switch
a variable out of the overloaded 'eax' variable to make room for read-byte-buffered.
- I'm starting to use SubX as an escape hatch for features missing in Mu:
- access to syscalls (which pass args in registers)
- access to global variables
How did new-literal ever work?! Somehow we had eax silently being clobbered
without affecting behavior over like 5 apps. Unsafe languages suck.
Anyways, factorial.mu is now part of CI.
So far it's unclear how to do this in a series of small commits. Still
nibbling around the edges. In this commit we standardize some terminology:
The length of an array or stream is denominated in the high-level elements.
The _size_ is denominated in bytes.
The thing we encode into the type is always the size, not the length.
There's still an open question of what to do about the Mu `length` operator.
I'd like to modify it to provide the length. Currently it provides the
size. If I can't fix that I'll rename it.
At the lowest level, SubX without syntax sugar uses names without prepositions.
For example, 01 and 03 are both called 'add', irrespective of source and
destination operand. Horizontal space is at a premium, and we rely on the
comments at the end of each line to fully describe what is happening.
Above that, however, we standardize on a slightly different naming convention
across:
a) SubX with syntax sugar,
b) Mu, and
c) the SubX code that the Mu compiler emits.
Conventions, in brief:
- by default, the source is on the left and destination on the right.
e.g. add %eax, 1/r32/ecx ("add eax to ecx")
- prepositions reverse the direction.
e.g. add-to %eax, 1/r32/ecx ("add ecx to eax")
subtract-from %eax, 1/r32/ecx ("subtract ecx from eax")
- by default, comparisons are left to right while 'compare<-' reverses.
Before, I was sometimes swapping args to make the operation more obvious,
but that would complicate the code-generation of the Mu compiler, and it's
nice to be able to read the output of the compiler just like hand-written
code.
One place where SubX differs from Mu: copy opcodes are called '<-' and
'->'. Hopefully that fits with the spirit of Mu rather than the letter
of the 'copy' and 'copy-to' instructions.
At the SubX level we have to put up with null-terminated kernel strings
for commandline args. But so far we haven't done much with them. Rather
than try to support them we'll just convert them transparently to standard
length-prefixed strings.
In the process I realized that it's not quite right to treat the combination
of argc and argv as an array of kernel strings. Argc counts the number
of elements, whereas the length of an array is usually denominated in bytes.
I built this in 3 phases:
a) create a helper in the bootstrap VM to render the state of the stack.
b) interactively arrive at the right function (tools/stack_array.subx)
c) pull the final solution into the standard library (093stack_allocate.subx)
As the final layer says, this may not be the fastest approach for most
(or indeed any) Mu programs. Perhaps it's better on balance for the compiler
to just emit n/4 `push` instructions.
(I'm sure this solution can be optimized further.)
We can't do it during parsing time because we may not have all type definitions
available yet. Mu supports using types before defining them.
At first I thought I should do it in populate-mu-type-sizes (appropriately
renamed). But there's enough complexity to tracking when stuff lands on
the stack that it's easiest to do while emitting code.
I don't think we need this information earlier in the compiler. If I'm
right, it seems simpler to colocate the computation of state close to where
it's used.
Move out total-size computation from parsing to a separate phase.
I don't have any new tests yet, but it's encouraging that existing tests
continue to pass.
This may be the first time I've ever written this much machine code (with
mutual recursion!) and gotten it to work the first time.