commit 7aae1a82b27a74605f0988f3350dad7ba87264f8
parent d500f29ba56b38180cca8c7a3f9be1367746be2c
Author: Virgil Dupras <hsoft@hardcoded.net>
Date: Tue, 21 Jun 2022 11:22:27 -0400
cc: straighten out binop generation and SF offsets
I was a bit cowboy-like with my design of stack frames. Things are a bit icky
when we consider that in x86, EBP points directly to the PS TOS (rather than
EBP+4). This complicates things. See doc/x86.
I don't think I'll go with making the TOS point to EBP+4, but I'll at least
contain this ickiness to cc/vm. Right now, it leaks in cc/map and cc/gen.
Diffstat:
5 files changed, 92 insertions(+), 12 deletions(-)
diff --git a/fs/cc/gen.fs b/fs/cc/gen.fs
@@ -3,6 +3,49 @@
\ Code generation
+\ This unit takes an AST and generates native code using cc/vm operations. We
+\ do so by starting from the root Unit node, iterate children (Functions) and
+\ recursively "resolve" them.
+
+\ As a general rule, we stay with Op1 (in cc/vm) active. Op2 usage is an
+\ exception. However, it's very important that "regular" node handler *don't*
+\ explicitely select Op1, because when we select Op2 for these exceptional
+\ reasons, we generally want to to this recursively, all the way down. This is
+\ why we don't see "selop" calls except in those exceptional places. We always
+\ target the "active" op.
+
+\ Binary Op resolution strategy
+\ The Binary Op node generation logic a few lines below needs a bit of an
+\ explanation. A binary op needs the two VM operands at once, apply an operation
+\ on them and return the result in Op1. Yes, Op1, not "active" op.
+
+\ A binary op can have 3 basic configuration for its two children:
+\ 1. Both children are "single ops" (or lvalues, or constants... nothing that
+\ requires 2 ops.
+\ 2. One of the child is or contains a node that needs 2 ops and the other is
+\ simple.
+\ 3. Both children are or contain nodes that need 2 ops.
+
+\ The first configuration is easy to solve, no need for anything. You resolve
+\ the first in op1, the second in op2, then apply the operation.
+
+\ The second configuration needs careful threading. Because the binop requires
+\ both VM operands, it should be executed first. If the binop is "left" (the
+\ first child), then nothing special needs to be done, Op1 has the proper value.
+\ If the binop is "right", then we need to swap Op1 and Op2 after having
+\ resolved it so that its result sits in the proper Op.
+
+\ The third configuration is the tricky one. If nothing is done, the result of
+\ the first binop will be overwritten by the calculation made by the second
+\ binop. We need to:
+\ 1. resolve the first node
+\ 2. save the result
+\ 3. resolve the second node
+\ 4. swap the result to Op2
+\ 5. restore the result to Op1.
+
+\ cc/vm abstracts away the save/restore mechanism through oppush/oppop.
+
: _err ( node -- ) printast abort" unexpected node" ;
alias noop gennode ( node -- ) \ forward declaration
@@ -39,6 +82,12 @@ alias noop gennode ( node -- ) \ forward declaration
( n1 n2 sz2 sz1 ) < if swap then ( node-to-adjust pointer-node )
node*arisz swap node*=n then ;
+\ Does node need 2 VM operands?
+: needs2ops? ( node -- f )
+ dup astid dup AST_BINARYOP = swap AST_FUNCALL = or if drop 1 exit then
+ firstchild begin ?dup while dup needs2ops? not while nextsibling repeat
+ ( needs2ops? == true ) drop 1 else ( end of children ) 0 then ;
+
UOPSCNT wordtbl uopgentbl ( -- )
:w ( - ) vmneg, ;
:w ( ~ ) vmnot, ;
@@ -103,22 +152,31 @@ ASTIDCNT wordtbl gentbl ( node -- )
'w genchildren ( ArgSpecs )
:w ( LValue ) lvvar vmap.sfoff sf+>op ;
:w ( UnaryOp )
+ _debug if ." unaryop: " dup printast nl> .ops then
dup genchildren
data1 uopgentbl swap wexec ;
:w ( PostfixOp )
dup genchildren
data1 popgentbl swap wexec ;
+\ See "Binary op resolution strategy" in opening comment
:w ( BinaryOp )
- ( node ) >r
- selop2 noop# selop1 optype if oppush 1 else 0 then ( reg? f )
- r@ childcount 2 = not if abort" binop node with more than 2 children!" then
+ _debug if ." binop: " dup printast nl> .ops then
+ selectedop >r ( node ) >r
r@ bopgentblpre r@ data1 wexec ( node )
firstchild dup nextsibling swap ( n2 n1 )
- selop1 gennode selop2 gennode bopgentblpost r> data1 wexec
- ( reg? f ) if op1<>op2 selop1 oppop then ;
+ over needs2ops? if \ n2 == 2ops
+ \ Resolve n2 before n1
+ swap gennode \ result in op1
+ dup needs2ops? if \ both need 2ops
+ oppush rot gennode selop2 oppop else
+ selop2 gennode op1<>op2 then
+ else \ nothing special needed, regular resolution
+ selop1 gennode selop2 gennode then
+ bopgentblpost r> data1 wexec
+ r> ( selectedop ) if op1<>op2 else selop1 then ;
'w _err ( unused )
:w ( If )
- firstchild ?dup not if _err then dup selop1 gennode ( exprnode )
+ firstchild ?dup not if _err then dup gennode ( exprnode )
vmjz, swap ( jump_addr exprnode ) ops$
nextsibling ?dup not if _err then dup gennode ( jump_addr condnode )
nextsibling ?dup if ( jump_addr elsenode )
diff --git a/fs/cc/map.fs b/fs/cc/map.fs
@@ -47,10 +47,10 @@ newxdict curmap
here swap , 16 allot0 ( entry ) ;
: Variable ( dnode -- )
+ dup data2 ( dnode type ) typesize over data3 ( nbelem ) * ( dnode sfsize )
+ curmap @ fmap.sfsize+ ( dnode )
dup data1 curmap @ fmap.vmap xentry ( dnode )
- curmap @ fmap.sfsize , dup , ( dnode )
- dup data2 ( dnode type ) typesize swap data3 ( nbelem ) * ( sfsize )
- curmap @ fmap.sfsize+ ;
+ curmap @ fmap.sfsize 4 - , , ;
: findvarinmap ( name funcentry -- varentry )
fmap.vmap xfind not if _err then ;
diff --git a/fs/cc/vm.fs b/fs/cc/vm.fs
@@ -85,12 +85,17 @@ create operands 16 allot0
operands value 'curop
: selop1 ( -- ) operands to 'curop ;
: selop2 ( -- ) operands 8 + to 'curop ;
+: selectedop ( -- n ) \ 0 == Op1 1 == Op2
+ 'curop operands = not ;
: optype ( -- type ) 'curop @ ;
: optype! ( type -- ) 'curop ! ;
: oparg ( -- arg ) 'curop 4 + @ ;
: oparg! ( arg -- ) 'curop 4 + ! ;
+
+\ reinitialize selected op to VM_NONE and dealloc registers if needed
: opdeinit optype $f and VM_REGISTER = if regfree then VM_NONE optype! ;
-\ reinitialize both ops to VM_NONE and dealloc registers if needed
+
+\ Deinit both ops and select Op1
: ops$
selop2 opdeinit selop1 opdeinit
reglvl if abort" unbalanced reg allot/free" then
diff --git a/fs/doc/x86.txt b/fs/doc/x86.txt
@@ -1,14 +1,30 @@
# x86 architecture
-The x86 nkernel source code is /dusk.asm. Register roles:
+The x86 kernel source code is /dusk.asm. Register roles:
PSP: EBP
RSP: ESP
All other registers are free.
-For now, this nkernel needs to run on a Linux kernel and uses its syscalls
+For now, this kernel needs to run on a Linux kernel and uses its syscalls
for user interaction and file reading.
It includes the boot source in its data section and makes boot< point to it
at initialization.
+
+## EBP and PS
+
+It's important to note that EBP points to PSP TOS *directly*. This means that
+you access the TOS with [ebp], the second element with [ebp+4] and so on.
+
+This is a bit confusing with regards to the C stack frame. There is constantly
+a 4 byte offset to consider. Here's a visual example of a 16 bytes wide stack
+frame
+
+--------|--------|--------|--------|--------|--------|--------|
+ ebp+16 | ebp+12 | ebp+8 | ebp+4 | ebp+0 | ebp-4 | ebp-8 |
+--------|--------|--------|--------|--------|--------|--------|
+ ^ ^
+ |--------------------------------------------|
+ 16 bytes stack frame
diff --git a/fs/tests/cc/cc.fs b/fs/tests/cc/cc.fs
@@ -22,4 +22,5 @@ ptrset 54 #eq
exprparens 9 #eq
cnoop ( no result! ) scnt 0 #eq
42 ptrari 46 #eq
+\ array 54 #eq BROKEN: b = a+1 computes at the wrong level (b = *a+1)
testend