duskos

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

commit f2e6fdd29f00b842d40def5c0297e96cf9be58eb
parent 21cfece551c5285569588b624faba78018bd41dd
Author: Virgil Dupras <hsoft@hardcoded.net>
Date:   Fri, 16 Sep 2022 10:21:01 -0400

cc/vm: consolidate and clarify code

Diffstat:
Dfs/cc/vm/common.fs | 67-------------------------------------------------------------------
Afs/cc/vm/commonhi.fs | 3+++
Afs/cc/vm/commonlo.fs | 76++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mfs/cc/vm/forth.fs | 110+++++++++++++++++++++++++++++++++++++++++--------------------------------------
Mfs/cc/vm/i386.fs | 51+++++++++++++++++++++++++--------------------------
Mfs/cc/vm/vm.fs | 1+
6 files changed, 162 insertions(+), 146 deletions(-)

diff --git a/fs/cc/vm/common.fs b/fs/cc/vm/common.fs @@ -1,67 +0,0 @@ -\ Code common to all VM implementations - -: _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. - -\ Operands definition and selection -\ Locations constant: where is the op located? -$00 const VM_NONE \ nowhere -$01 const VM_CONSTANT \ a constant of value "arg" -$02 const VM_STACKFRAME \ on RS at RSP+arg -$03 const VM_ARGSFRAME \ on PS at PSP+arg -$04 const VM_REGISTER \ in an implementation-specific register of id "arg" -$05 const VM_CONSTARRAY \ pointer to an array with the 1st elem being length -\ Below, references to a location (points to X) -$11 const VM_*CONSTANT -$12 const VM_*STACKFRAME -$13 const VM_*ARGSFRAME -$14 const VM_*REGISTER - -struct[ VMOp - sfield loc \ one of the VM_ constants - sfield arg - 4 &+ 'arg - : :deinit SZ 0 fill ; - : :loclo loc $f and ; - : :deref? loc $10 and bool ; - : :noop# loc VM_NONE = _assert ; - : :keep ( self -- arg loc ) dup arg swap loc ; - : :push ( self -- arg loc ) dup >r :keep VM_NONE to r> loc ; - : :pop ( arg loc self -- ) dup >r :noop# r@ to loc r> to arg ; -]struct - -create operands VMOp SZ 2 * allot -operands structbind VMOp vmop -operands VMOp SZ + structbind VMOp vmop^ \ the "other" op -0 value opsunsigned \ when 1, ops are considered unsigned - -: selop1 ( -- ) operands ['] vmop rebind operands VMOp SZ + ['] vmop^ rebind ; -: selop2 ( -- ) operands ['] vmop^ rebind operands VMOp SZ + ['] vmop rebind ; -: selectedop ( -- n ) \ 0 == Op1 1 == Op2 - vmop :self operands <> ; -: selop^ selectedop if selop1 else selop2 then ; -: opsunsigned! 1 to opsunsigned ; - -: .ops selectedop .x1 spc> 4 >r operands begin @+ .x spc> next drop nl> ; -: _ops$ selop2 vmop :deinit selop1 vmop :deinit 0 to opsunsigned ; - -\ Managing operands - -: hasop# vmop loc VM_NONE = not _assert ; -: isconst? vmop loc VM_CONSTANT = ; -: isconst# isconst? _assert ; -: noop# vmop :noop# ; -: const>op ( n -- ) noop# VM_CONSTANT to vmop loc to vmop arg ; -: constarray>op ( a -- ) noop# VM_CONSTARRAY to vmop loc to vmop arg ; -: sf+>op ( off -- ) noop# VM_*STACKFRAME to vmop loc to vmop arg ; -: ps+>op ( off -- ) noop# VM_*ARGSFRAME to vmop loc to vmop arg ; -: mem>op ( n -- ) noop# VM_*CONSTANT to vmop loc to vmop arg ; - -\ Swap op1 and op2 locs/args -: op1<>op2 - vmop :push vmop^ :push vmop :pop vmop^ :pop ; diff --git a/fs/cc/vm/commonhi.fs b/fs/cc/vm/commonhi.fs @@ -0,0 +1,3 @@ +\ Code common to all VM implementations (high part) + +: ops$ selop2 vmop :init selop1 vmop :init 0 to opsunsigned neutral# ; diff --git a/fs/cc/vm/commonlo.fs b/fs/cc/vm/commonlo.fs @@ -0,0 +1,76 @@ +\ Code common to all VM implementations (low part) + +\ Note: there can only be one implementation of the VM loaded in memory at once. +\ Loading another VM impl will break the previous one. In Dusk, it's not a +\ problem because there is no CC cross-compiling. You'll always want to compile +\ with one VM: your CPU's. + +: _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. + +\ Operands definition and selection +\ Locations constant: where is the op located? +$00 const VM_NONE \ nowhere +$01 const VM_CONSTANT \ a constant of value "arg" +$02 const VM_STACKFRAME \ on RS at RSP+arg +$03 const VM_ARGSFRAME \ on PS at PSP+arg +$04 const VM_REGISTER \ in an implementation-specific register of id "arg" +$05 const VM_CONSTARRAY \ pointer to an array with the 1st elem being length +\ Below, references to a location (points to X) +$11 const VM_*CONSTANT +$12 const VM_*STACKFRAME +$13 const VM_*ARGSFRAME +$14 const VM_*REGISTER + +struct[ VMOp + sfield loc \ one of the VM_ constants + sfield arg + sfield other \ link to the "other" op + 4 &+ 'arg + \ Initialize op to VM_NONE, "freeing" any resource it held. + : :init VM_NONE swap to loc ; + : :loclo loc $f and ; + : :deref? loc $10 and bool ; + : :noop# loc VM_NONE = _assert ; + : :keep ( self -- arg loc ) dup arg swap loc ; + : :push ( self -- arg loc ) dup >r :keep VM_NONE to r> loc ; + : :pop ( arg loc self -- ) dup >r :noop# r@ to loc r> to arg ; +]struct + +create operands VMOp SZ 2 * allot +operands structbind VMOp vmop +operands VMOp SZ + structbind VMOp vmop^ \ the "other" op +vmop^ :self to vmop other +vmop :self to vmop^ other +0 value opsunsigned \ when 1, ops are considered unsigned + +: _sel ['] vmop rebind vmop other ['] vmop^ rebind ; +: selop1 ( -- ) operands _sel ; +: selop2 ( -- ) operands VMOp SZ + _sel ; +: selectedop ( -- n ) \ 0 == Op1 1 == Op2 + vmop :self operands <> ; +: selop^ vmop other _sel ; +: opsunsigned! 1 to opsunsigned ; + +: .ops selectedop .x1 spc> 4 >r operands begin @+ .x spc> next drop nl> ; + +\ Managing operands + +: hasop# vmop loc VM_NONE = not _assert ; +: isconst? vmop loc VM_CONSTANT = ; +: isconst# isconst? _assert ; +: noop# vmop :noop# ; +: const>op ( n -- ) noop# VM_CONSTANT to vmop loc to vmop arg ; +: constarray>op ( a -- ) noop# VM_CONSTARRAY to vmop loc to vmop arg ; +: sf+>op ( off -- ) noop# VM_*STACKFRAME to vmop loc to vmop arg ; +: ps+>op ( off -- ) noop# VM_*ARGSFRAME to vmop loc to vmop arg ; +: mem>op ( n -- ) noop# VM_*CONSTANT to vmop loc to vmop arg ; + +\ Swap op1 and op2 locs/args +: op1<>op2 + vmop :push vmop^ :push vmop :pop vmop^ :pop ; diff --git a/fs/cc/vm/forth.fs b/fs/cc/vm/forth.fs @@ -2,7 +2,7 @@ \ This VM produces slower and bigger code than their native counterpart but has \ the advantage of working under any architecture. -?f<< /cc/vm/common.fs +?f<< /cc/vm/commonlo.fs \ How the VM works \ Generally, CPUs have registers that we want to use for performance reasons. @@ -20,43 +20,46 @@ $06 const VM_TOS $16 const VM_*TOS -: opdeinit VM_NONE to vmop loc ; -: opfree vmop :loclo VM_TOS = if compile drop then opdeinit ; - -: ops$ selop2 opfree selop1 opfree _ops$ ; - -: TOS# vmop^ :loclo VM_TOS <> _assert ; -: op>TOS VM_TOS to vmop loc ; -: op>*TOS VM_*TOS to vmop loc ; - \ When accumulating call arguments, we need to keep track of how many we have \ and apply the corresponding offset to VM_ARGSFRAME. 0 value psoff +: PS+ 4 to+ psoff ; +: PS- -4 to+ psoff ; -: _ ( oploc -- ) - case - VM_CONSTANT of = vmop arg litn endof - VM_STACKFRAME of = vmop arg r', endof - VM_ARGSFRAME of = - vmop arg vmop^ :loclo VM_TOS = if CELLSZ + then psoff + p', endof - VM_REGISTER of = vmop 'arg litn compile @ endof - VM_TOS of = endof \ nothing to do - _err endcase ; - -\ Resolve current operand and compile a push to PS as either VM_TOS or VM_*TOS. -: opCompile ( -- ) - vmop :deref? if vmop :loclo _ op>*TOS else vmop loc _ op>TOS then ; +struct+[ VMOp + : :TOS? :loclo VM_TOS = ; + \ Re-initialize op, "forgetting" the TOS, abandoning it to PS. + : :forgetTOS dup :TOS? _assert VMOp :init ; + : :init dup :TOS? if compile drop then VMOp :init ; + : :>TOS VM_TOS swap to loc ; + : :>*TOS VM_*TOS swap to loc ; + : _ ( loc self -- ) + swap case + VM_CONSTANT of = arg litn endof + VM_STACKFRAME of = arg r', endof + VM_ARGSFRAME of = + dup arg swap other :loclo VM_TOS = if CELLSZ + then psoff + p', endof + VM_REGISTER of = 'arg litn compile @ endof + VM_TOS of = drop endof \ nothing to do + _err endcase ; + \ Resolve current operand and compile a push to PS as either VM_TOS or + \ VM_*TOS. + : :compile& >r + r@ :deref? if r@ :loclo r@ _ r> :>*TOS else r@ loc r@ _ r> :>TOS then ; + \ Resolve current operand and forces dereferencing. Always yields VM_TOS. + : :compile ( -- ) >r + r@ :compile& r@ loc VM_*TOS = if compile @ r@ :>TOS then rdrop ; +]struct -\ Resolve current operand and forces dereferencing. Always yields VM_TOS. -: opCompile* ( -- ) - opCompile vmop loc VM_*TOS = if compile @ op>TOS then ; +\ Verify that we're in "neutral" position with regards to PS +: neutral# psoff if abort" unbalanced PS" then ; \ transform current operand in its reference : ?&op>op ( -- f ) vmop :deref? if vmop :loclo to vmop loc 1 else 0 then ; : &op>op ?&op>op _assert ; : op>reg vmop :loclo VM_REGISTER = if exit then - hasop# ?&op>op ( f ) opCompile vmop 'arg litn compile ! ( f ) + hasop# ?&op>op ( f ) vmop :compile& vmop 'arg litn compile ! ( f ) 4 lshift VM_REGISTER or to vmop loc ; \ If any of the op is VM_TOS, push it to a register. @@ -65,7 +68,7 @@ $16 const VM_*TOS \ dereference current operand : *op>op vmop :deref? if - opCompile* op>*TOS op>reg else + vmop :compile vmop :>*TOS op>reg else vmop loc $10 or to vmop loc then .ops ; \ We override the common :push/:pop mechanism to adjust to the weirness of @@ -80,7 +83,7 @@ $16 const VM_*TOS \ a TOS. If it is, change it into a REGISTER. struct+[ VMOp : :push ( self -- arg loc ) - dup :loclo VM_REGISTER = if opCompile then VMOp :push ; + dup :loclo VM_REGISTER = if vmop :compile& then VMOp :push ; : :pop ( arg loc self -- ) over $f and VM_TOS = if ?tos>reg then VMOp :pop ; ]struct @@ -95,7 +98,7 @@ struct+[ VMOp : vmret, selop2 noop# \ returning with a second operand? something's wrong selop1 vmop loc if - opCompile* opdeinit argsz ?dup if + vmop :compile vmop :forgetTOS argsz ?dup if p', compile ! argsz CELLSZ - ?dup if p+, then then else argsz ?dup if p+, then then @@ -103,20 +106,21 @@ struct+[ VMOp exit, ; \ Write op to args -: vmcallarg, ( -- ) opCompile* opdeinit 4 to+ psoff ; +: vmcallarg, ( -- ) vmop :compile vmop :forgetTOS PS+ ; \ Call the address in current op. If the function has a result, you need to \ pop it with vmpspop, : vmcall, ( -- ) VM_*CONSTANT vmop loc = if - vmop arg execute, else opCompile* compile execute then - opdeinit 0 to psoff ; + vmop arg execute, vmop :init + else vmop :compile vmop :forgetTOS compile execute then + 0 to psoff ; \ Allocate a new register for active op and pop 4b from PS into it. -: vmpspop, noop# op>TOS op>reg ; +: vmpspop, noop# vmop :>TOS op>reg ; \ Push active op to PS. -: vmpspush, opCompile* opdeinit ; +: vmpspush, vmop :compile vmop :forgetTOS ; \ Unary operations are performed on the selected op, which can be either op1 or \ op2. @@ -124,9 +128,9 @@ struct+[ VMOp tuck @ swap execute ( a n ) swap ! ; : unop doer ' , does> @ ( w ) isconst? if vmop arg swap execute to vmop arg else - litn 4 to+ psoff \ the litn! - vmop :keep &op>op opCompile* opdeinit vmop :pop compile apply - -4 to+ psoff then ; + litn PS+ \ the litn! + vmop :keep &op>op vmop :compile vmop :forgetTOS vmop :pop compile apply + PS- then ; unop vmneg, neg unop vmnot, ^ \ ~ @@ -139,8 +143,8 @@ unop vm--op, 1- \ We need to copy the old value to TOS and then inc or dec the reference. : apply ( w a -- n ) \ Same as unop's apply, but yield old value tuck @ ( a w old ) dup rot execute ( a old new ) rot ! ; -: postop doer ' , does> @ ( w ) litn 4 to+ psoff \ the litn! - &op>op opCompile* opdeinit compile apply op>TOS -4 to+ psoff ; +: postop doer ' , does> @ ( w ) litn PS+ \ the litn! + &op>op vmop :compile vmop :forgetTOS compile apply vmop :>TOS PS- ; postop vmop++, 1+ postop vmop--, 1- @@ -151,9 +155,9 @@ postop vmop--, 1- \ 2 fields: signed op, unsigned op : binop doer ' , ' , does> opsunsigned if CELLSZ + then @ ( w ) - selop1 opCompile* \ op1 is TOS + selop1 vmop :compile \ op1 is TOS selop2 hasop# swapifTOS - opCompile* vmop :deinit \ op2 is "lost" on PS + vmop :compile vmop :forgetTOS ( w ) execute, selop1 ; \ result in op1 as VM_TOS binop vmadd, + + @@ -174,11 +178,11 @@ binop vm||, or and \ with the participation of op2. : binop= doer ' , does> @ ( w ) >r selop1 vmop :keep r> ( ... w ) - opCompile* \ op1 is TOS - selop2 hasop# opCompile* \ op2 is TOS + vmop :compile \ op1 is TOS + selop2 hasop# vmop :compile \ op2 is TOS ( w ) execute, \ result on TOS - selop1 vmop :deinit - vmop :pop &op>op opCompile* vmop :deinit vmop^ :deinit compile ! ; + selop1 vmop :forgetTOS + vmop :pop &op>op vmop :compile vmop :forgetTOS vmop^ :forgetTOS compile ! ; binop= vm<<=, lshift binop= vm>>=, rshift @@ -187,23 +191,23 @@ binop= vm>>=, rshift \ op2. In other words, perform a AST_ASSIGN with the right part as op2 \ and the left part as op1. : _movarray, \ special case, we have a {1, 2, 3} assign - selop1 vmop loc VM_STACKFRAME = _assert opCompile* \ dst on compiled stack - opdeinit selop2 vmop arg @+ ( a len ) >r begin ( a ) + selop1 vmop loc VM_STACKFRAME = _assert vmop :compile \ dst on PS + vmop :forgetTOS selop2 vmop arg @+ ( a len ) >r begin ( a ) @+ litn compile swap compile !+ next compile drop ( a ) drop - opdeinit ; + vmop :init ; : vmmov, selop2 hasop# vmop loc VM_CONSTARRAY = if _movarray, else - opCompile* \ op2 is TOS - selop1 swapifTOS &op>op opCompile* - vmop :deinit vmop^ :deinit compile ! then ; + vmop :compile \ op2 is TOS + selop1 swapifTOS &op>op vmop :compile + vmop :forgetTOS vmop^ :forgetTOS compile ! then ; \ Jumping : ]vmjmp ( 'jump_addr -- ) here swap ! ; : vmjmp, ( a -- ) [compile] again ; : vmjmp[, ( -- a ) compile (br) here 4 allot ; \ In conditional jumps below, the source of the test in in current op -: _ opCompile* opdeinit ; +: _ vmop :compile vmop :forgetTOS ; : vmjz, ( a -- ) _ [compile] until ; : vmjz[, ( -- a ) _ [compile] if ; : vmjnz, ( a -- ) _ compile not [compile] until ; diff --git a/fs/cc/vm/i386.fs b/fs/cc/vm/i386.fs @@ -41,7 +41,7 @@ \ For usage example, see tests/cc/vm.fs ?f<< asm/i386.fs -?f<< /cc/vm/common.fs +?f<< /cc/vm/commonlo.fs \ Register management \ When an operand needs to go to a register, we allocate one for it. when it @@ -65,14 +65,13 @@ create registers AX c, BX c, CX c, DX c, SI c, DI c, reglvl not if abort" too many regfree" then -1 to+ reglvl reglvl REGCNT >= if ax pop, then ; -\ reinitialize selected op to VM_NONE and dealloc registers if needed -: opdeinit vmop :loclo VM_REGISTER = if regfree then VM_NONE to vmop loc ; +struct+[ VMOp + \ reinitialize selected op to VM_NONE and dealloc registers if needed + : :init dup :loclo VM_REGISTER = if regfree then VMOp :init ; +]struct -\ Deinit both ops and select Op1 -: ops$ - selop2 opdeinit selop1 opdeinit - reglvl if abort" unbalanced reg allot/free" then - _ops$ ; +\ Verify that we're in "neutral" position with regards to registers +: neutral# reglvl if abort" unbalanced reg allot/free" then ; \ Resolve current operand as an assembler "src" argument. : opAsm ( -- ) @@ -160,14 +159,14 @@ create registers AX c, BX c, CX c, DX c, SI c, DI c, \ Write op to args : vmcallarg, ( -- ) - opderef 4 to+ callsz callsz neg bp d) opAsm mov, opdeinit ; + opderef 4 to+ callsz callsz neg bp d) opAsm mov, vmop :init ; \ Call the address in current op. If the function has a result, you need to \ pop it with vmpspop, : vmcall, ( -- ) callsz ?dup if bp i) sub, 0 to callsz then VM_*CONSTANT vmop loc = if vmop arg VM_NONE to vmop loc else opAsm then - abs>rel call, opdeinit ; + abs>rel call, vmop :init ; \ TODO: copy forth VM's TOS argtype and logic to all VMs. This could save quite \ a few back-and-forth operations. @@ -178,20 +177,20 @@ create registers AX c, BX c, CX c, DX c, SI c, DI c, bp CELLSZ i) add, ; \ Push active op to PS. -: vmpspush, opderef bp CELLSZ i) sub, bp 0 d) opAsm mov, opdeinit ; +: vmpspush, opderef bp CELLSZ i) sub, bp 0 d) opAsm mov, vmop :init ; \ 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 ; -: vm&, binopprep and, opdeinit ; -: vm|, binopprep or, opdeinit ; -: vm^, binopprep xor, opdeinit ; +: vmadd, binopprep add, vmop :init ; +: vmsub, binopprep sub, vmop :init ; +: vm&, binopprep and, vmop :init ; +: vm|, binopprep or, vmop :init ; +: vm^, binopprep xor, vmop :init ; \ TODO: allow non-const shift right-operand -: vm<<, binopprep isconst# shl, opdeinit ; -: vm>>, binopprep isconst# shr, opdeinit ; +: vm<<, binopprep isconst# shl, vmop :init ; +: vm>>, binopprep isconst# shr, vmop :init ; \ 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 @@ -201,7 +200,7 @@ create registers AX c, BX c, CX c, DX c, SI c, DI c, \ 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 vmop arg AX = not if ax push, ax opAsm mov, then - selop2 op>reg hasop# opAsm mul, opdeinit + selop2 op>reg hasop# opAsm mul, vmop :init selop1 vmop arg AX = not if opAsm ax mov, ax pop, then reglvl 4 >= if dx pop, then ; \ Copy the contents of op2 in the memory address pointed out by op1 and deinit @@ -215,12 +214,12 @@ create registers AX c, BX c, CX c, DX c, SI c, DI c, drop selop2 else maybederef selop1 opAsm selop2 opAsm mov, then - opdeinit ; + vmop :init ; : binop=prep ( -- ) \ prepare ops a binop of the "assign" loc selop1 opAsm selop2 hasop# opAsm ; -: vm<<=, binop=prep isconst# shl, opdeinit ; -: vm>>=, binop=prep isconst# shr, opdeinit ; +: vm<<=, binop=prep isconst# shl, vmop :init ; +: vm>>=, binop=prep isconst# shr, vmop :init ; \ Code generation - Unary ops \ Unary operations are performed on the selected op, which can be either op1 or @@ -250,18 +249,18 @@ create registers AX c, BX c, CX c, DX c, SI c, DI c, : _ ( 'w -- ) selop1 vmop loc VM_*STACKFRAME = vmop loc VM_*ARGSFRAME = or _assert selop2 noop# selop1 vmop loc vmop arg selop2 to vmop arg to vmop loc - selop1 op>reg selop2 opAsm execute opdeinit selop1 ; + selop1 op>reg selop2 opAsm execute vmop :init selop1 ; : vmop++, ['] inc, _ ; : vmop--, ['] dec, _ ; \ Code generation - Logic : _ - selop1 op>reg opAsm selop2 opAsm cmp, opdeinit + selop1 op>reg opAsm selop2 opAsm cmp, vmop :init selop1 opAsm 0 i) mov, ; : vm<, _ opAsm opsunsigned if setb, else setl, then ; : vm==, _ opAsm setz, ; -: _ ( 'w -- ) selop1 opAsm selop2 opAsm execute opdeinit selop1 vmboolify, ; +: _ ( 'w -- ) selop1 opAsm selop2 opAsm execute vmop :init selop1 vmboolify, ; : vm&&, ['] and, _ ; : vm||, ['] or, _ ; @@ -288,7 +287,7 @@ create registers AX c, BX c, CX c, DX c, SI c, DI c, \ test ..., -1. : vmtest, opAsm vmop loc $f and VM_REGISTER = if opAsm else -1 i) then test, ( sets Z ) - opdeinit ; + vmop :init ; : vmjz, ( a -- ) selop1 vmtest, jz, ; : vmjz[, ( -- a ) 0 vmjz, _ ; : vmjnz, ( a -- ) selop1 vmtest, jnz, ; diff --git a/fs/cc/vm/vm.fs b/fs/cc/vm/vm.fs @@ -3,3 +3,4 @@ require sys/scratch.fs S" /cc/vm" curpath :find# ( path ) syspad :[ ARCH c@+ dup 3 + c, move, ," .fs" syspad :] ( path fname ) swap Path :child Path :fload +f<< /cc/vm/commonhi.fs