duskos

dusk os fork
git clone git://git.alexwennerberg.com/duskos
Log | Files | Refs | README | LICENSE

commit fe384925332a6b205fb5648450d5502c2b7335c1
parent 8fb909c8bdd8783e55e9f37474fd8ca9bc26d677
Author: Virgil Dupras <hsoft@hardcoded.net>
Date:   Sat, 18 Jun 2022 14:44:47 -0400

cc: completely overhaul cc/vm

My first draft turned out to be a bit more complex than it should and didn't
generate very good code. This new design generated much, much better code. It's
more sophisticated, but I think it's simpler as a whole with its 2 ops approach
instead of op+result.

Diffstat:
Mfs/asm.fs | 15+++++++++++----
Mfs/cc/gen.fs | 60+++++++++++++++++++++++++++---------------------------------
Mfs/cc/vm.fs | 349+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
Mfs/tests/cc/vm.fs | 114+++++++++++++++++++++++++++++++++++++------------------------------------------
4 files changed, 306 insertions(+), 232 deletions(-)

diff --git a/fs/asm.fs b/fs/asm.fs @@ -38,12 +38,14 @@ : isimm? ( -- f ) src _id IMM = ; : tgt-or-src! tgt 0< if to tgt else to src then ; -: r! ( reg -- ) $300 or ( mod 3 ) tgt-or-src! ; +: r! ( reg -- ) $300 or ( mod 3 ) tgt-or-src! ; : [r]! ( reg -- ) ( mod 0 ) tgt-or-src! ; : [r]+8b! ( reg -- ) $100 or ( mod 1 ) tgt-or-src! ; : eax AX r! ; alias eax ax alias eax al : ebx BX r! ; alias ebx bx alias ebx bl +: ecx CX r! ; alias ecx cx alias ecx cl +: edx DX r! ; alias edx dx alias edx dl : ebp BP r! ; : edi DI r! ; : [eax] AX [r]! ; @@ -63,7 +65,7 @@ prefix, op, ( reg ) 3 lshift tgtid tgt mod or or ( modrm ) c, disp? if disp c, then asm$ ; : modrm<imm, ( imm immreg op -- ) \ immediate src, modrm tgt - op, 3 lshift tgtid or tgt mod or ( modrm ) c, , disp, asm$ ; + op, 3 lshift tgtid or tgt mod or ( modrm ) c, disp, , asm$ ; : modrm2, ( imm? reg op -- ) \ modrm op with 2 arguments src mod $c0 = not if \ surprise! it's not tgt that is the mod rm, it's src. We're in "alternate" @@ -104,5 +106,10 @@ $58 op pop, $50 op push, \ Special : mov, prefix, isimm? if - $b8 tgtid or c, , asm$ else - src $89 modrm2, then ; + tgt mod $c0 = if \ mod 3, simple imm->reg move + $b8 tgtid or c, , asm$ else \ imm->modrm move + 0 $c7 modrm<imm, then + else src $89 modrm2, then ; + +: shln, ( n -- ) \ SHL n times + 4 $c1 modrm1, c, ; diff --git a/fs/cc/gen.fs b/fs/cc/gen.fs @@ -6,17 +6,17 @@ : _err ( node -- ) printast abort" unexpected node" ; UOPSCNT wordtbl uopgentbl ( -- ) -:w ( - ) operand?>result vmneg, ; -:w ( ~ ) operand?>result vmnot, ; -:w ( ! ) operand?>result vmboolnot, ; -'w operand>&operand ( & ) -'w operand>[operand] ( * ) -:w ( ++ ) operand?>result vminc, ; -:w ( -- ) operand?>result vmdec, ; +:w ( - ) vmneg, ; +:w ( ~ ) vmnot, ; +:w ( ! ) vmboolnot, ; +'w &op>op ( & ) +'w *op>op ( * ) +:w ( ++ ) vm++op, ; +:w ( -- ) vm--op, ; POPSCNT wordtbl popgentbl ( -- ) -:w ( ++ ) vminc, ; -:w ( -- ) vmdec, ; +:w ( ++ ) vmop++, ; +:w ( -- ) vmop--, ; BOPSCNT wordtbl bopgentblmiddle ( node -- node ) 'w noop ( + ) @@ -55,25 +55,21 @@ alias noop gennode ( node -- ) \ forward declaration : spit ( a u -- ) A>r >r >A begin Ac@+ .x1 next r>A ; : getfuncmap ( node -- funcentry ) AST_FUNCTION parentnodeid data2 ; -\ get SF offset of AST_LVALUE node -: lvsfoff ( lvnode -- off ) - dup data1 swap getfuncmap ( name funcentry ) findvarinmap ( varentry ) - vmap.sfoff ; +: lvvar ( lvnode -- varentry ) + dup data1 swap getfuncmap ( name funcentry ) findvarinmap ; \ special binop case : _assign ( node -- ) firstchild ?dup not if _err then ( lvnode ) dup nextsibling ?dup not if _err then ( lvnode exprnode ) - gennode operand?>result \ result=set - gennode \ operand=set - result>operand ; + selop1 gennode selop2 gennode op1<>op2 op2>*op1 ; ASTIDCNT wordtbl gentbl ( node -- ) 'w drop ( Declare ) 'w genchildren ( Unit ) :w ( Function ) _debug if ." debugging: " dup data1 stype nl> then - vm$ + ops$ dup data1 entry dup data2 ( astfunc mapfunc ) here over fmap.address! \ set address @@ -81,13 +77,13 @@ ASTIDCNT wordtbl gentbl ( node -- ) genchildren _debug if current here current - spit nl> then ; :w ( Return ) - genchildren operand?>result vmret, ; -:w ( Constant ) data1 const>operand ; + genchildren vmret, ; +:w ( Constant ) data1 const>op ; :w ( Statements ) - \ we run vm$ between each statement to discard any unused Result - firstchild ?dup if begin dup gennode vm$ nextsibling ?dup not until then ; + \ we run ops$ between each statement to discard any unused Result + firstchild ?dup if begin dup gennode ops$ nextsibling ?dup not until then ; 'w genchildren ( ArgSpecs ) -:w ( LValue ) lvsfoff sf+>operand ; +:w ( LValue ) lvvar vmap.sfoff sf+>op ; :w ( UnaryOp ) dup genchildren data1 uopgentbl swap wexec ; @@ -96,32 +92,30 @@ ASTIDCNT wordtbl gentbl ( node -- ) data1 popgentbl swap wexec ; :w ( BinaryOp ) dup data1 12 = if _assign exit then ( 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 r@ firstchild dup nextsibling swap ( n2 n1 ) - gennode bopgentblmiddle r@ data1 wexec - operand?>result - resultset? if - pushresult, gennode operand?>result popresult, else - gennode operand?>result then - bopgentblpost r> data1 wexec ; + selop1 gennode bopgentblmiddle r@ data1 wexec + selop2 gennode bopgentblpost r> data1 wexec + ( reg? f ) if op1<>op2 selop1 oppop then ; 'w _err ( unused ) :w ( If ) - firstchild ?dup not if _err then dup gennode ( exprnode ) - operand?>result vmjz, swap ( jump_addr exprnode ) + firstchild ?dup not if _err then dup selop1 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 ) - vmjnz, ( ja1 enode ja2 ) rot vmjmp! ( enode ja2 ) + vmjmp, ( ja1 enode ja2 ) rot vmjmp! ( enode ja2 ) swap gennode ( ja2 ) then ( jump_addr ) vmjmp! ; 'w _err ( unused ) :w ( FunCall ) \ pass arguments dup childcount 4 * callargallot, dup firstchild ?dup if -4 swap begin ( cursf+ argnode ) - dup gennode operand?>result swap dup sf+>operand result>operand + dup selop1 gennode swap dup selop2 sf+>op op1<>op2 op2>*op1 ops$ 4 - swap nextsibling ?dup not until drop then \ find and call ( node ) data1 ( name ) findfuncinmap ( mapfunc ) - fmap.address vmcall>result, ; + fmap.address vmcall>op1, ; : _ ( node -- ) gentbl over astid wexec ; current to gennode diff --git a/fs/cc/vm.fs b/fs/cc/vm.fs @@ -4,123 +4,179 @@ \ The goal of this VM is to provide a unified API for code generation of a C \ AST across CPU architecture. -\ Computation done by this generated code is centered around the "Result". -\ concept. The goal is to make the CPU move bits around towards that Result. -\ The Result always lives on a particular register reserved for this role. For -\ example, on x86, it's EAX. +\ Computation done by this generated code is centered around two operands, Op1 +\ and Op2. Those operands can "live" in different places depending on the +\ context: in a register, in memory, or as a constant. -\ The VM very often interacts with the Result through another concept: the -\ Operand. Moving from the operand to the result, from the result to the -\ operand, running an operation on both the Result and the operand, etc. +\ Each of the two operands can be of either of those types: -\ The Operand doesn't live in a particular register, it comes from multiple -\ types of sources. Its possible values are: - -\ None: no operand specified +\ None: operand not specified \ Constant: a constant value \ Stack Frame: an address on the Stack Frame -\ Register: value currently being held in an "alternate" register (EBX on x86) +\ Register: value currently being held a register + +\ Besides the type, each operand has an accompanying "argument", whose meaning +\ depend on the type: + +\ Constant: the value of the constant +\ Stack Frame: the offset relative to the SF pointer +\ Register: the ID of the register -\ We operate the VM by first specifying an operand, moving the operand toward -\ the result, performing ops, moving the result out. If we need to keep an -\ intermediate result for later, we can push it to a stack (the mechanism for -\ this is arch-specific). The pushed result can be later pulled backed into the -\ Operand. +\ On those operands, the VM generates code that perform operations on them. +\ Although some operations are special, there are basically 2 types of +\ operations: unary op and binary op. + +\ When performing an unary op we: +\ 1. Assert that op2 is unset +\ 2. if op1 is not in a register, move the value to a register +\ 3. Perform the operation on the register + +\ When performing an binary op we: +\ 1. Assert that op2 is set +\ 2. if op1 is not in a register, move the value to a register +\ 3. Perform the binary operation with op1 as the target, op2 as the source. \ To avoid errors, moving an operand to a non-empty and non-pushed Result is an \ error. To set the operand when it's not None is also an error. \ For usage example, see tests/cc/vm.fs +: _err abort" vm error" ; +: _assert not if _err then ; + +\ Execution context (function) + +0 value argsz \ size of the argument portion of the SF. +0 value locsz \ size of the "local vars" portion of the SF. +0 value callsz \ size of the args portion of a Function Call + +\ Register management +\ When an operand needs to go to a register, we allocate one for it. when it +\ doesn't need it anymore, we deallocate it. Registers have to be deallocated +\ in the reverse order they were allocated. +\ Allocation works by having a list of register to allocate, and a pointer, +\ "reglvl" which indicate which register can be used next. When we run out of +\ register, allocating a register pushes the first register to the stack and +\ then allocates it. + +6 const REGCNT +create registers AX c, BX c, CX c, DX c, SI c, DI c, +0 value reglvl + +: curreg ( -- regid ) + reglvl REGCNT < if + registers reglvl + c@ ( regid ) else + eax push, AX ( regid ) then ; +: regallot ( -- regid ) curreg 1 to+ reglvl ; +: regfree ( -- ) + reglvl not if abort" too many regfree" then + -1 to+ reglvl reglvl REGCNT >= if eax pop, then ; + +\ Operands definition and selection $00 const VM_NONE $01 const VM_CONSTANT $02 const VM_STACKFRAME $03 const VM_REGISTER +$13 const VM_*REGISTER \ dereferenced value pointed by register -0 value resultset? \ 0 = no result, 1=result set -VM_NONE value operand -\ For VM_CONSTANT, this contains the actual value -\ For VM_STACKFRAME, this contains the SF offset -0 value operandarg -0 value operandlvl \ -1 means "&", 1 means "*", 2 means "**" etc. -0 value argsz \ size of the argument portion of the SF. -0 value locsz \ size of the "local vars" portion of the SF. -0 value callsz \ size of the args portion of a Function Call +\ 2 operands, 2 fields each (type, arg), 4b per field +create operands 16 allot0 -: vm$ 0 to resultset? VM_NONE to operand 0 to operandlvl 0 to callsz ; +operands value 'curop +: selop1 ( -- ) operands to 'curop ; +: selop2 ( -- ) operands 8 + to 'curop ; +: optype ( -- type ) 'curop @ ; +: optype! ( type -- ) 'curop ! ; +: oparg ( -- arg ) 'curop 4 + @ ; +: oparg! ( arg -- ) 'curop 4 + ! ; +: opdeinit optype $f and VM_REGISTER = if regfree then VM_NONE optype! ; +\ reinitialize both ops to VM_NONE and dealloc registers if needed +: ops$ + selop2 opdeinit selop1 opdeinit + reglvl if abort" unbalanced reg allot/free" then + operands 16 0 fill ; -: _err abort" vm error" ; -: _assert not if _err then ; +\ Managing operands + +: hasop# optype VM_NONE = not _assert ; +: noop# optype VM_NONE = _assert ; +: const>op ( n -- ) noop# VM_CONSTANT optype! oparg! ; +: sf+>op ( off -- ) noop# VM_STACKFRAME optype! oparg! ; \ get current operand SF offset, adjusted with callsz -: opersf+ ( -- off ) operandarg callsz + ; +: opsf+ ( -- off ) oparg callsz + ; \ Resolve current operand as an assembler "src" argument. -: operandAsmKeep ( -- ) - operand VM_CONSTANT = if - operandarg i32 - else operand VM_REGISTER = if - ebx - else operand VM_STACKFRAME = if - opersf+ [ebp]+ - else _err then then then ; - -: operandAsm ( -- ) operandAsmKeep VM_NONE to operand ; - -: result! 1 to resultset? ; - -\ Force current operand to be copied to the "alternate" register -: operand>reg ( -- ) - operand VM_REGISTER = not if - ebx operandAsm mov, VM_REGISTER to operand then ; - -\ emit, if necessary, the code necessary to resolve "positive" operandlvl -: resolvederef - operandlvl if - operandlvl 0< if - 0 to operandlvl - VM_REGISTER to operand - ebx ebp mov, - opersf+ ?dup if ebx i32 add, then - else operand>reg begin - ebx [ebx] mov, -1 to+ operandlvl operandlvl not until then then ; - -: const>operand ( n -- ) - VM_NONE operand = _assert - VM_CONSTANT to operand to operandarg ; - -: sf+>operand ( offset -- ) - VM_NONE operand = _assert - VM_STACKFRAME to operand to operandarg ; - -: operand>result ( -- ) - resultset? not _assert - resolvederef - eax operandAsm mov, result! ; - -: operand?>result operand VM_NONE = not if operand>result then ; - -: operand>&operand - operand VM_STACKFRAME = _assert - operandlvl 0>= _assert - -1 to operandlvl ; - -: operand>[operand] - operand VM_STACKFRAME = operand VM_REGISTER = or _assert - 1 to+ operandlvl ; - -: result>operand - resultset? _assert - operand VM_STACKFRAME = if - operandlvl if - -1 to+ operandlvl operand>reg resolvederef - [ebx] eax mov, - else operandAsm eax mov, then - else operand VM_REGISTER = if - -1 to+ operandlvl resolvederef - [ebx] eax mov, - else _err then then - 0 to resultset? VM_NONE to operand ; +: opAsm ( -- ) + optype case + VM_CONSTANT of = oparg i32 endof + VM_REGISTER of = oparg r! endof + VM_STACKFRAME of = opsf+ [ebp]+ endof + VM_*REGISTER of = oparg [r]! endof + _err endcase ; + +\ Force current operand to be copied to a register +: op>reg + hasop# optype VM_REGISTER = not if + regallot dup r! ( regid ) opAsm mov, oparg! VM_REGISTER optype! then ; + +\ Resolve any referencing into a "simple" result. A VM_STACKFRAME goes into a +\ register, a VM_*REGISTER is resolved into a VM_REGISTER. +: opderef + optype case + VM_STACKFRAME of = op>reg endof + VM_*REGISTER of = oparg r! oparg [r]! mov, VM_REGISTER optype! endof + endcase ; + +\ Before doing an operation on two operands, we verify that they are compatible. +\ For example, we can't have two VM_*REGISTER ops. one of them has to be +\ dereferenced (it has to be op2). +: maybederef + selop1 optype VM_*REGISTER = optype VM_STACKFRAME = or if + selop2 opderef then ; + +\ Resolve current operand as an assembler "tgt" argument. +: *opAsm ( -- ) + optype case + VM_STACKFRAME of = opsf+ [ebp]+ endof + VM_*REGISTER of = oparg [r]! endof + _err endcase ; + +\ if possible, transform current operand in its reference +: &op>op optype case + VM_STACKFRAME of = + regallot dup r! ebp mov, dup r! opsf+ i32 add, + oparg! VM_REGISTER optype! endof + VM_*REGISTER of = VM_REGISTER optype! endof + _err endcase ; + +\ if possible, dereference current operand +: *op>op optype case + VM_STACKFRAME of = op>reg *op>op endof + VM_REGISTER of = VM_*REGISTER optype! endof + VM_*REGISTER of = opderef VM_*REGISTER optype! endof + _err endcase ; + +\ Force the op into a register and then reset the op to VM_NONE +: oppush ( -- regid ) op>reg VM_NONE optype! oparg ; + +\ Assuming that current op is VM_NONE, set it back to VM_REGISTER, its arg to +\ the current register at reglvl +: oppop ( regid -- ) noop# VM_REGISTER optype! oparg! ; + +\ Copy the contents of op2 in the memory address pointed out by op1 and deinit +\ op2. In other words, perform a AST_ASSIGN with the right part as op2 +\ and the left part as op1. +: op2>*op1 + maybederef selop1 *opAsm selop2 opAsm mov, opdeinit ; + +\ Swap op1 and op2 types/args +: op1<>op2 + selop1 optype oparg + selop2 oparg swap oparg! optype rot optype! + selop1 optype! oparg! ; + +\ Code generation - Functions, calls, ret \ generate function prelude code by allocating "locsz" bytes on PS. : vmprelude, ( argsz locsz -- ) @@ -129,54 +185,79 @@ VM_NONE value operand \ deallocate locsz and argsz. If result is set, keep a 4b in here and push the \ result there. : vmret, - locsz argsz + resultset? if 4 - then + selop2 noop# \ returning with a second operand? something's wrong + locsz argsz + selop1 optype if 4 - then + opderef \ for [ebp] src mov, to work, "src" has to be "simple" ?dup if ebp i32 add, then - resultset? if [ebp] eax mov, then + optype if [ebp] opAsm mov, then ret, ; -: pushresult, resultset? _assert eax push, 0 to resultset? ; -: popresult, - VM_NONE operand = _assert - VM_REGISTER to operand - ebx pop, ; -: callargallot, ( bytes -- ) - dup to callsz ebp i32 sub, ; + +: callargallot, ( bytes -- ) dup to callsz ebp i32 sub, ; \ TODO: call function with no return value. \ call addr and assume that it left a 4b area in the SF with its result in it. \ move this data in Result. -: vmcall>result, ( addr -- ) - resultset? not _assert +: vmcall>op1, ( addr -- ) + selop1 noop# call, - eax [ebp] mov, 1 to resultset? + VM_REGISTER optype! regallot r! [ebp] mov, ebp 4 i32 add, 0 to callsz ; -: vmadd, eax operandAsm add, result! ; -: vmsub, eax operandAsm sub, result! ; -: vmmul, operand>reg operandAsm mul, result! ; -: vmneg, eax neg, ; -: vmnot, ( ~ ) eax not, ; -: vmboolnot, - eax eax test, - eax 0 i32 mov, - al setz, ; -\ inc/dec have 2 operation modes. If there's no operand, it inc/dec the result. -\ if there's an operand, it *post* inc/dec, that is, it moves the operand to -\ the result and then it inc/dec the operand. -: _ ( 'w -- ) operand VM_NONE = if - eax execute else - eax operandAsmKeep result! mov, operandAsm execute then ; -: vminc, ['] inc, _ ; -: vmdec, ['] dec, _ ; -: vm<, - eax operandAsm cmp, - eax 0 i32 mov, - al setg, ; -: vm==, - eax operandAsm cmp, - eax 0 i32 mov, - al setz, ; + + +\ Code generation - Binary ops +: binopprep ( -- ) \ prepare ops for the binop + selop1 op>reg opAsm + selop2 hasop# opAsm ; +: vmadd, binopprep add, opdeinit ; +: vmsub, binopprep sub, opdeinit ; +\ mul is special and cannot use binopprep for two reasons: its target operand +\ is hardcoded to EAX and also, EDX gets written by the op, so we need to save +\ EDX if in use. +: vmmul, + reglvl 4 >= if edx push, then + \ if op1 is not EAX, we need to push EAX, perform the mul, copy EAX to op1's + \ reg, then pop eax back. + selop1 op>reg oparg AX = not if eax push, eax opAsm mov, then + selop2 op>reg hasop# opAsm mul, opdeinit + selop1 oparg AX = not if opAsm eax mov, eax pop, then + reglvl 4 >= if edx pop, then ; + +\ Code generation - Unary ops +: unaryopprep selop2 noop# selop1 op>reg opAsm ; +: vmneg, unaryopprep neg, ; +: vmnot, ( ~ ) unaryopprep not, ; +: vmboolnot, unaryopprep + opAsm test, + opAsm 0 i32 mov, + opAsm setz, ; + +\ pre-inc/dec op1 +\ TODO: *opAsm goes below, not opAsm. We must inc the reference, not the result. +: vm++op, selop2 noop# selop1 opAsm inc, ; +: vm--op, selop2 noop# selop1 opAsm dec, ; + +\ post-inc/dec op1 +\ It's a bit complicated here. Before we inc/dec, we need a copy of the current +\ value in a new register, which will be our result. +\ For now, we only support op1=VM_STACKFRAME +: _ ( 'w -- ) + selop1 optype VM_STACKFRAME = _assert + selop2 noop# selop1 optype oparg selop2 oparg! optype! + selop1 op>reg selop2 *opAsm execute opdeinit selop1 ; +: vmop++, ['] inc, _ ; +: vmop--, ['] dec, _ ; + +\ Code generation - Logic + +: _ + selop1 op>reg opAsm selop2 opAsm cmp, opdeinit + selop1 opAsm 0 i32 mov, ; +: vm<, _ opAsm setl, ; +: vm==, _ opAsm setz, ; : vmjmp! ( 'jump_addr -- ) here over - 4 - swap ! ; +: vmjmp, 0 jmp, here 4 - ; : vmjz, ( -- addr ) - eax eax test, 0 to resultset? + selop1 opAsm opAsm test, opdeinit 0 jz, here 4 - ; : vmjnz, ( -- addr ) - eax eax test, 0 to resultset? + selop1 opAsm opAsm test, opdeinit 0 jnz, here 4 - ; diff --git a/fs/tests/cc/vm.fs b/fs/tests/cc/vm.fs @@ -3,117 +3,109 @@ f<< asm.fs f<< cc/vm.fs testbegin \ Tests for the C compiler VM module - \ binop[+](binop[*](const[2],const[3]),const[1]) -vm$ +ops$ code test1 0 0 vmprelude, - 2 const>operand - operand>result - 3 const>operand + selop1 2 const>op + selop2 3 const>op vmmul, - 1 const>operand + selop2 1 const>op vmadd, vmret, test1 7 #eq \ binop[+](binop[-](const[3], const[1]),binop[*](const[2],const[3])) -vm$ +ops$ code test2 0 0 vmprelude, - 3 const>operand - operand>result - 1 const>operand + selop1 3 const>op + selop2 1 const>op vmsub, - pushresult, - 2 const>operand - operand>result - 3 const>operand + selop1 oppush + selop1 2 const>op + selop2 3 const>op vmmul, - popresult, + selop2 oppop vmadd, vmret, test2 8 #eq \ sub 2 args -vm$ +ops$ code test3 8 0 vmprelude, - 4 sf+>operand - operand>result - 0 sf+>operand + selop1 4 sf+>op + selop2 0 sf+>op vmsub, vmret, 54 12 test3 42 #eq \ assign 2 local vars -vm$ +ops$ code test4 0 8 vmprelude, \ foo = 42 - 42 const>operand - operand>result - 4 sf+>operand - result>operand + selop2 42 const>op + selop1 4 sf+>op + op2>*op1 + ops$ \ bar = 5 - 5 const>operand - operand>result - 0 sf+>operand - result>operand + selop2 5 const>op + selop1 0 sf+>op + op2>*op1 + ops$ \ return foo + bar - 4 sf+>operand - operand>result - 0 sf+>operand + selop1 4 sf+>op + selop2 0 sf+>op vmadd, vmret, test4 47 #eq \ variable reference and dereference -vm$ +ops$ code test5 0 8 vmprelude, \ foo = 42 - 42 const>operand - operand>result - 4 sf+>operand - result>operand + selop2 42 const>op + selop1 4 sf+>op + op2>*op1 + ops$ \ bar = &foo - 4 sf+>operand - operand>&operand - operand>result - 0 sf+>operand - result>operand + selop2 4 sf+>op + &op>op + selop1 0 sf+>op + op2>*op1 + ops$ \ return *bar - 0 sf+>operand - operand>[operand] - operand>result + selop1 0 sf+>op + *op>op vmret, test5 42 #eq \ assign and dereference -vm$ +ops$ code test6 0 8 vmprelude, \ foo = 42 - 42 const>operand - operand>result - 4 sf+>operand - result>operand + selop2 42 const>op + selop1 4 sf+>op + op2>*op1 + ops$ \ bar = &foo - 4 sf+>operand - operand>&operand - operand>result - 0 sf+>operand - result>operand + selop2 4 sf+>op + &op>op + selop1 0 sf+>op + op2>*op1 + ops$ \ *bar = 54 - 54 const>operand - operand>result - 0 sf+>operand - operand>[operand] - result>operand + selop2 54 const>op + selop1 0 sf+>op + *op>op + op2>*op1 + ops$ \ return foo - 4 sf+>operand - operand>result + selop1 4 sf+>op vmret, test6 54 #eq testend