diff options
author | Ben Bridle <ben@derelict.engineering> | 2025-09-12 17:20:19 +1200 |
---|---|---|
committer | Ben Bridle <ben@derelict.engineering> | 2025-09-12 17:28:44 +1200 |
commit | 19c5679b8be4fe879e165576e97f96b85f5b5044 (patch) | |
tree | de10dbc9a23514adbddfd563d38b1238dfce6241 | |
parent | 4c67a10f265dde1c3aa430cb851062a38646d6f2 (diff) | |
download | bedrock-js-19c5679b8be4fe879e165576e97f96b85f5b5044.zip |
Implement a faster core in WebAssembly
This is a massive commit that restructures a lot of the library. The
primary change is the implementation of a second Bedrock core using
WebAssembly, which performs much better than the existing JavaScript
core. The JavaScript core has been retained as a fallback for browsers
that don't support WebAssembly.
Benchmarking both cores using the numbers benchmark and with all of the
devices stubbed out in the emulator demonstrates a 40x speedup for the
WebAssembly implementation (going from 4800ms to 120ms).
-rw-r--r-- | bedrock-core.wat | 484 | ||||
-rw-r--r-- | bedrock.js | 2211 |
2 files changed, 1728 insertions, 967 deletions
diff --git a/bedrock-core.wat b/bedrock-core.wat new file mode 100644 index 0000000..4aa8173 --- /dev/null +++ b/bedrock-core.wat @@ -0,0 +1,484 @@ +(module + ;; Import device functions from JavaScript. + (import "bedrock" "dget1" (func $dget1 (param $p i32) (result i32))) + (import "bedrock" "dset1" (func $dset1 (param $p i32) (param $v i32) (result i32))) + + ;; We instantiate and export two pages of memory (65536*2 bytes). + ;; The first 65535 bytes are program memory, the next 256 bytes are the + ;; working stack, and the following 256 bytes are the return stack. + ;; Stacks will be downward-growing so that we can push and pop doubles + ;; at a time with a single 16-bit instruction (access is little-endian). + (memory (export "memory") 2) + + ;; Cycle counter and pointer declarations. Stack pointers are downward-growing + ;; and point to the highest byte on the stack, not to the next empty byte. + (global $cc (mut i64) (i64.const 0)) + (global $ip (mut i32) (i32.const 0)) + (global $wp (mut i32) (i32.const 0x10100)) + (global $rp (mut i32) (i32.const 0x10200)) + ;; Memory offset constants. + (global $ip-base i32 (i32.const 0)) + (global $wp-base i32 (i32.const 0x10100)) + (global $rp-base i32 (i32.const 0x10200)) + ;; Signal constants. + (global $SIGNAL.BREAK i32 (i32.const 0)) + (global $SIGNAL.HALT i32 (i32.const 1)) + (global $SIGNAL.DB1 i32 (i32.const 2)) + (global $SIGNAL.DB2 i32 (i32.const 3)) + (global $SIGNAL.DB3 i32 (i32.const 4)) + (global $SIGNAL.DB4 i32 (i32.const 5)) + (global $SIGNAL.DB5 i32 (i32.const 6)) + (global $SIGNAL.DB6 i32 (i32.const 7)) + (global $SIGNAL.SLEEP i32 (i32.const 8)) + (global $SIGNAL.RESET i32 (i32.const 9)) + (global $SIGNAL.FORK i32 (i32.const 10)) + + ;; Re-initialise pointers. + (func (export "reset") + (global.set $ip (global.get $ip-base)) + (global.set $wp (global.get $wp-base)) + (global.set $rp (global.get $rp-base)) ) + + ;; Provide external access to normal pointer values. + (func (export "cc") (result i64) global.get $cc ) + (func (export "ip") (result i32) global.get $ip ) + (func (export "wp") (result i32) (i32.sub (global.get $wp-base) (global.get $wp)) ) + (func (export "rp") (result i32) (i32.sub (global.get $rp-base) (global.get $rp)) ) + + ;; Push value micro-instructions. + (func $wpsh1 (export "wpsh1") (param $v i32) + (global.set $wp (i32.sub (global.get $wp) (i32.const 1))) + (i32.store8 (global.get $wp) (local.get $v)) ) + (func $wpsh2 (export "wpsh2") (param $v i32) + (global.set $wp (i32.sub (global.get $wp) (i32.const 2))) + (i32.store16 (global.get $wp) (local.get $v)) ) + (func $rpsh1 (export "rpsh1") (param $v i32) + (global.set $rp (i32.sub (global.get $rp) (i32.const 1))) + (i32.store8 (global.get $rp) (local.get $v)) ) + (func $rpsh2 (export "rpsh2") (param $v i32) + (global.set $rp (i32.sub (global.get $rp) (i32.const 2))) + (i32.store16 (global.get $rp) (local.get $v)) ) + (func $wpshb (export "wpshb") (param $b i32) + (select (i32.const 0xFF) (i32.const 0) (local.get $b)) + (call $wpsh1) ) + (func $rpshb (export "rpshb") (param $b i32) + (select (i32.const 0xFF) (i32.const 0) (local.get $b)) + (call $rpsh1) ) + ;; Pop value micro-instructions. + (func $wpop1 (export "wpop1") (result i32) + (i32.load8_u (global.get $wp)) + (global.set $wp (i32.add (global.get $wp) (i32.const 1))) ) + (func $wpop2 (export "wpop2") (result i32) + (i32.load16_u (global.get $wp)) + (global.set $wp (i32.add (global.get $wp) (i32.const 2))) ) + (func $rpop1 (export "rpop1") (result i32) + (i32.load8_u (global.get $rp)) + (global.set $rp (i32.add (global.get $rp) (i32.const 1))) ) + (func $rpop2 (export "rpop2") (result i32) + (i32.load16_u (global.get $rp)) + (global.set $rp (i32.add (global.get $rp) (i32.const 2))) ) + (func $mpop1 (export "mpop1") (result i32) + (i32.load8_u (global.get $ip)) + (global.set $ip (i32.add (global.get $ip) (i32.const 1))) ) + (func $mpop2 (export "mpop2") (result i32) + (i32.shl (i32.load8_u (global.get $ip)) (i32.const 8)) + (i32.load8_u (i32.add (global.get $ip) (i32.const 1))) + (i32.or) + (global.set $ip (i32.add (global.get $ip) (i32.const 2))) ) + ;; Get value micro-instructions. + (func $wget1 (export "wget1") (result i32) + (i32.load8_u (global.get $wp)) ) + (func $wget2 (export "wget2") (result i32) + (i32.load16_u (global.get $wp)) ) + (func $rget1 (export "rget1") (result i32) + (i32.load8_u (global.get $rp)) ) + (func $rget2 (export "rget2") (result i32) + (i32.load16_u (global.get $rp)) ) + (func $mget1 (export "mget1") (param $a i32) (result i32) + (i32.load8_u (local.get $a)) ) + (func $mget2 (export "mget2") (param $a i32) (result i32) + (i32.shl (i32.load8_u (local.get $a)) (i32.const 8)) + (i32.load8_u (i32.add (local.get $a) (i32.const 1))) + (i32.or) ) + (func $dget2 (export "dget2") (param $p i32) (result i32) + (i32.shl (call $dget1 (local.get $p)) (i32.const 8)) + (call $dget1 (i32.add (local.get $p) (i32.const 1))) + (i32.or) ) + ;; Set value micro-instructions. + (func $mset1 (export "mset1") (param $a i32) (param $v i32) + (i32.store8 (local.get $a) (local.get $v)) ) + (func $mset2 (export "mset2") (param $a i32) (param $v i32) + (i32.store8 (local.get $a) (i32.shr_u (local.get $v) (i32.const 8))) + (i32.store8 (i32.add (local.get $a) (i32.const 1)) (i32.and (local.get $v) (i32.const 0xFF))) ) + (func $dset2 (export "dset2") (param $p i32) (param $v i32) (result i32) + (call $dset1 (local.get $p) (i32.shr_u (local.get $v) (i32.const 8))) + (call $dset1 (i32.add (local.get $p) (i32.const 1)) (i32.and (local.get $v) (i32.const 0xFF))) + (i32.or) ) ;; TODO: Signals are or'd together, at least one must be zero for this to work. + ;; Pointer movement micro-instructions. + (func $ipmov (export "ipmov") (param $d i32) + (global.set $ip (i32.add (global.get $ip) (local.get $d))) ) + (func $wpmov (export "wpmov") (param $d i32) + (global.set $wp (i32.sub (global.get $wp) (local.get $d))) ) + (func $rpmov (export "rpmov") (param $d i32) + (global.set $rp (i32.sub (global.get $rp) (local.get $d))) ) + ;; Math operations + (func $rol1 (export "rol1") (param $v i32) (param $d i32) (result i32) + (local.set $d (i32.and (local.get $d) (i32.const 0x7))) + (i32.shl (local.get $v) (local.get $d)) + (i32.shr_u (local.get $v) (i32.sub (i32.const 8) (local.get $d))) + (i32.or) ) + (func $ror1 (export "ror1") (param $v i32) (param $d i32) (result i32) + (local.set $d (i32.and (local.get $d) (i32.const 0x7))) + (i32.shl (local.get $v) (i32.sub (i32.const 8) (local.get $d))) + (i32.shr_u (local.get $v) (local.get $d)) + (i32.or) ) + (func $rol2 (export "rol2") (param $v i32) (param $d i32) (result i32) + (local.set $d (i32.and (local.get $d) (i32.const 0xF))) + (i32.shl (local.get $v) (local.get $d)) + (i32.shr_u (local.get $v) (i32.sub (i32.const 16) (local.get $d))) + (i32.or) ) + (func $ror2 (export "ror2") (param $v i32) (param $d i32) (result i32) + (local.set $d (i32.and (local.get $d) (i32.const 0xF))) + (i32.shl (local.get $v) (i32.sub (i32.const 16) (local.get $d))) + (i32.shr_u (local.get $v) (local.get $d)) + (i32.or) ) + + + ;; TODO: Add a max-cycles parameter, and also keep track of total cycles with an i64. + (func (export "eval") (param $c i32) (result i32) + ;; Declare local variables. + (local $m i64) ;; end mark + (local $x i32) ;; register + + (local.set $m (i64.add (global.get $cc) (i64.extend_i32_u (local.get $c)))) + + (loop $loop + (block $break + ;; Increment the cycle counter if we haven't reached our mark. + (if (i64.eq (local.get $m) (global.get $cc)) (then (br $break))) + (global.set $cc (i64.add (global.get $cc) (i64.const 1))) + + ;; Switch statement for every instruction variant. The list is in reverse order. + (block $NOTr*: (block $ANDr*: (block $XORr*: (block $IORr*: (block $RORr*: (block $ROLr*: (block $SHRr*: (block $SHLr*: + (block $NQKr*: (block $EQUr*: (block $GTHr*: (block $LTHr*: (block $DECr*: (block $INCr*: (block $SUBr*: (block $ADDr*: + (block $STDr*: (block $LDDr*: (block $STAr*: (block $LDAr*: (block $JCSr*: (block $JCNr*: (block $JMSr*: (block $JMPr*: + (block $ROTr*: (block $SWPr*: (block $OVRr*: (block $DUPr*: (block $CPYr*: (block $POPr*: (block $PSHr*: (block $DB6 + (block $NOTr* (block $ANDr* (block $XORr* (block $IORr* (block $RORr* (block $ROLr* (block $SHRr* (block $SHLr* + (block $NQKr* (block $EQUr* (block $GTHr* (block $LTHr* (block $DECr* (block $INCr* (block $SUBr* (block $ADDr* + (block $STDr* (block $LDDr* (block $STAr* (block $LDAr* (block $JCSr* (block $JCNr* (block $JMSr* (block $JMPr* + (block $ROTr* (block $SWPr* (block $OVRr* (block $DUPr* (block $CPYr* (block $POPr* (block $PSHr* (block $DB5 + (block $NOTr: (block $ANDr: (block $XORr: (block $IORr: (block $RORr: (block $ROLr: (block $SHRr: (block $SHLr: + (block $NQKr: (block $EQUr: (block $GTHr: (block $LTHr: (block $DECr: (block $INCr: (block $SUBr: (block $ADDr: + (block $STDr: (block $LDDr: (block $STAr: (block $LDAr: (block $JCSr: (block $JCNr: (block $JMSr: (block $JMPr: + (block $ROTr: (block $SWPr: (block $OVRr: (block $DUPr: (block $CPYr: (block $POPr: (block $PSHr: (block $DB4 + (block $NOTr (block $ANDr (block $XORr (block $IORr (block $RORr (block $ROLr (block $SHRr (block $SHLr + (block $NQKr (block $EQUr (block $GTHr (block $LTHr (block $DECr (block $INCr (block $SUBr (block $ADDr + (block $STDr (block $LDDr (block $STAr (block $LDAr (block $JCSr (block $JCNr (block $JMSr (block $JMPr + (block $ROTr (block $SWPr (block $OVRr (block $DUPr (block $CPYr (block $POPr (block $PSHr (block $DB3 + (block $NOT*: (block $AND*: (block $XOR*: (block $IOR*: (block $ROR*: (block $ROL*: (block $SHR*: (block $SHL*: + (block $NQK*: (block $EQU*: (block $GTH*: (block $LTH*: (block $DEC*: (block $INC*: (block $SUB*: (block $ADD*: + (block $STD*: (block $LDD*: (block $STA*: (block $LDA*: (block $JCS*: (block $JCN*: (block $JMS*: (block $JMP*: + (block $ROT*: (block $SWP*: (block $OVR*: (block $DUP*: (block $CPY*: (block $POP*: (block $PSH*: (block $DB2 + (block $NOT* (block $AND* (block $XOR* (block $IOR* (block $ROR* (block $ROL* (block $SHR* (block $SHL* + (block $NQK* (block $EQU* (block $GTH* (block $LTH* (block $DEC* (block $INC* (block $SUB* (block $ADD* + (block $STD* (block $LDD* (block $STA* (block $LDA* (block $JCS* (block $JCN* (block $JMS* (block $JMP* + (block $ROT* (block $SWP* (block $OVR* (block $DUP* (block $CPY* (block $POP* (block $PSH* (block $DB1 + (block $NOT: (block $AND: (block $XOR: (block $IOR: (block $ROR: (block $ROL: (block $SHR: (block $SHL: + (block $NQK: (block $EQU: (block $GTH: (block $LTH: (block $DEC: (block $INC: (block $SUB: (block $ADD: + (block $STD: (block $LDD: (block $STA: (block $LDA: (block $JCS: (block $JCN: (block $JMS: (block $JMP: + (block $ROT: (block $SWP: (block $OVR: (block $DUP: (block $CPY: (block $POP: (block $PSH: (block $NOP + (block $NOT (block $AND (block $XOR (block $IOR (block $ROR (block $ROL (block $SHR (block $SHL + (block $NQK (block $EQU (block $GTH (block $LTH (block $DEC (block $INC (block $SUB (block $ADD + (block $STD (block $LDD (block $STA (block $LDA (block $JCS (block $JCN (block $JMS (block $JMP + (block $ROT (block $SWP (block $OVR (block $DUP (block $CPY (block $POP (block $PSH (block $HLT + ;; Branch based on the instruction on the stack. + (br_table + $HLT $PSH $POP $CPY $DUP $OVR $SWP $ROT $JMP $JMS $JCN $JCS $LDA $STA $LDD $STD + $ADD $SUB $INC $DEC $LTH $GTH $EQU $NQK $SHL $SHR $ROL $ROR $IOR $XOR $AND $NOT + $NOP $PSH: $POP: $CPY: $DUP: $OVR: $SWP: $ROT: $JMP: $JMS: $JCN: $JCS: $LDA: $STA: $LDD: $STD: + $ADD: $SUB: $INC: $DEC: $LTH: $GTH: $EQU: $NQK: $SHL: $SHR: $ROL: $ROR: $IOR: $XOR: $AND: $NOT: + $DB1 $PSH* $POP* $CPY* $DUP* $OVR* $SWP* $ROT* $JMP* $JMS* $JCN* $JCS* $LDA* $STA* $LDD* $STD* + $ADD* $SUB* $INC* $DEC* $LTH* $GTH* $EQU* $NQK* $SHL* $SHR* $ROL* $ROR* $IOR* $XOR* $AND* $NOT* + $DB2 $PSH*: $POP*: $CPY*: $DUP*: $OVR*: $SWP*: $ROT*: $JMP*: $JMS*: $JCN*: $JCS*: $LDA*: $STA*: $LDD*: $STD*: + $ADD*: $SUB*: $INC*: $DEC*: $LTH*: $GTH*: $EQU*: $NQK*: $SHL*: $SHR*: $ROL*: $ROR*: $IOR*: $XOR*: $AND*: $NOT*: + $DB3 $PSHr $POPr $CPYr $DUPr $OVRr $SWPr $ROTr $JMPr $JMSr $JCNr $JCSr $LDAr $STAr $LDDr $STDr + $ADDr $SUBr $INCr $DECr $LTHr $GTHr $EQUr $NQKr $SHLr $SHRr $ROLr $RORr $IORr $XORr $ANDr $NOTr + $DB4 $PSHr: $POPr: $CPYr: $DUPr: $OVRr: $SWPr: $ROTr: $JMPr: $JMSr: $JCNr: $JCSr: $LDAr: $STAr: $LDDr: $STDr: + $ADDr: $SUBr: $INCr: $DECr: $LTHr: $GTHr: $EQUr: $NQKr: $SHLr: $SHRr: $ROLr: $RORr: $IORr: $XORr: $ANDr: $NOTr: + $DB5 $PSHr* $POPr* $CPYr* $DUPr* $OVRr* $SWPr* $ROTr* $JMPr* $JMSr* $JCNr* $JCSr* $LDAr* $STAr* $LDDr* $STDr* + $ADDr* $SUBr* $INCr* $DECr* $LTHr* $GTHr* $EQUr* $NQKr* $SHLr* $SHRr* $ROLr* $RORr* $IORr* $XORr* $ANDr* $NOTr* + $DB6 $PSHr*: $POPr*: $CPYr*: $DUPr*: $OVRr*: $SWPr*: $ROTr*: $JMPr*: $JMSr*: $JCNr*: $JCSr*: $LDAr*: $STAr*: $LDDr*: $STDr*: + $ADDr*: $SUBr*: $INCr*: $DECr*: $LTHr*: $GTHr*: $EQUr*: $NQKr*: $SHLr*: $SHRr*: $ROLr*: $RORr*: $IORr*: $XORr*: $ANDr*: $NOTr*: + ;; Load an instruction from memory. + (call $mpop1) ) + + ) (; HLT ;) (return (global.get $SIGNAL.HALT)) + ) (; PSH ;) (call $rpop1) (call $wpsh1) (br $loop) + ) (; POP ;) (call $wpmov (i32.const -1)) (br $loop) + ) (; CPY ;) (call $rget1) (call $wpsh1) (br $loop) + ) (; DUP ;) (call $wget1) (call $wpsh1) (br $loop) + ) (; OVR ;) (call $wpop1) (call $wget1) (local.set $x) (call $wpsh1) (local.get $x) (call $wpsh1) (br $loop) + ) (; SWP ;) (call $wpop1) (call $wpop1) (local.set $x) (call $wpsh1) (local.get $x) (call $wpsh1) (br $loop) + ) (; ROT ;) (call $wpop1) (call $wpop1) (call $wpop1) (local.set $x) (call $wpsh1) (call $wpsh1) (local.get $x) (call $wpsh1) (br $loop) + ) (; JMP ;) (call $wpop2) (global.set $ip) (br $loop) + ) (; JMS ;) (call $wpop2) (global.get $ip) (call $rpsh2) (global.set $ip) (br $loop) + ) (; JCN ;) (call $wpop2) (local.set $x) (if (call $wpop1) (then (local.get $x) (global.set $ip))) (br $loop) + ) (; JCS ;) (call $wpop2) (local.set $x) (if (call $wpop1) (then (global.get $ip) (call $rpsh2) (local.get $x) (global.set $ip))) (br $loop) + ) (; LDA ;) (call $wpop2) (call $mget1) (call $wpsh1) (br $loop) + ) (; STA ;) (call $wpop2) (call $wpop1) (call $mset1) (br $loop) + ) (; LDD ;) (call $wpop1) (call $dget1) (call $wpsh1) (br $loop) + ) (; STD ;) (call $wpop1) (call $wpop1) (call $dset1) (local.set $x) (if (local.get $x) (then (return (local.get $x)))) (br $loop) + ) (; ADD ;) (call $wpop1) (call $wpop1) (i32.add) (call $wpsh1) (br $loop) + ) (; SUB ;) (call $wpop1) (local.set $x) (call $wpop1) (local.get $x) (i32.sub) (call $wpsh1) (br $loop) + ) (; INC ;) (call $wpop1) (i32.add (i32.const 1)) (call $wpsh1) (br $loop) + ) (; DEC ;) (call $wpop1) (i32.sub (i32.const 1)) (call $wpsh1) (br $loop) + ) (; LTH ;) (call $wpop1) (call $wpop1) (i32.gt_u) (call $wpshb) (br $loop) + ) (; GTH ;) (call $wpop1) (call $wpop1) (i32.lt_u) (call $wpshb) (br $loop) + ) (; EQU ;) (call $wpop1) (call $wpop1) (i32.eq) (call $wpshb) (br $loop) + ) (; NQK ;) (call $wpop1) (local.tee $x) (call $wget1) (local.get $x) (call $wpsh1) (i32.ne) (call $wpshb) (br $loop) + ) (; SHL ;) (call $wpop1) (local.set $x) (call $wpop1) (local.get $x) (i32.shl) (call $wpsh1) (br $loop) + ) (; SHR ;) (call $wpop1) (local.set $x) (call $wpop1) (local.get $x) (i32.shr_u) (call $wpsh1) (br $loop) + ) (; ROL ;) (call $wpop1) (local.set $x) (call $wpop1) (local.get $x) (call $rol1) (call $wpsh1) (br $loop) + ) (; ROR ;) (call $wpop1) (local.set $x) (call $wpop1) (local.get $x) (call $ror1) (call $wpsh1) (br $loop) + ) (; IOR ;) (call $wpop1) (call $wpop1) (i32.or) (call $wpsh1) (br $loop) + ) (; XOR ;) (call $wpop1) (call $wpop1) (i32.xor) (call $wpsh1) (br $loop) + ) (; AND ;) (call $wpop1) (call $wpop1) (i32.and) (call $wpsh1) (br $loop) + ) (; NOT ;) (call $wpop1) (i32.xor (i32.const 0xFF)) (call $wpsh1) (br $loop) + + ) (; NOP ;) (br $loop) + ) (; PSH: ;) (call $mpop1) (call $wpsh1) (br $loop) + ) (; POP: ;) (call $ipmov (i32.const 1)) (br $loop) + ) (; CPY: ;) (call $mpop1) (local.tee $x) (call $wpsh1) (local.get $x) (call $rpsh1) (br $loop) + ) (; DUP: ;) (call $mpop1) (local.tee $x) (call $wpsh1) (local.get $x) (call $wpsh1) (br $loop) + ) (; OVR: ;) (call $mpop1) (call $wget1) (local.set $x) (call $wpsh1) (local.get $x) (call $wpsh1) (br $loop) + ) (; SWP: ;) (call $mpop1) (call $wpop1) (local.set $x) (call $wpsh1) (local.get $x) (call $wpsh1) (br $loop) + ) (; ROT: ;) (call $mpop1) (call $wpop1) (call $wpop1) (local.set $x) (call $wpsh1) (call $wpsh1) (local.get $x) (call $wpsh1) (br $loop) + ) (; JMP: ;) (call $mpop2) (global.set $ip) (br $loop) + ) (; JMS: ;) (call $mpop2) (global.get $ip) (call $rpsh2) (global.set $ip) (br $loop) + ) (; JCN: ;) (call $mpop2) (local.set $x) (if (call $wpop1) (then (local.get $x) (global.set $ip))) (br $loop) + ) (; JCS: ;) (call $mpop2) (local.set $x) (if (call $wpop1) (then (global.get $ip) (call $rpsh2) (local.get $x) (global.set $ip))) (br $loop) + ) (; LDA: ;) (call $mpop2) (call $mget1) (call $wpsh1) (br $loop) + ) (; STA: ;) (call $mpop2) (call $wpop1) (call $mset1) (br $loop) + ) (; LDD: ;) (call $mpop1) (call $dget1) (call $wpsh1) (br $loop) + ) (; STD: ;) (call $mpop1) (call $wpop1) (call $dset1) (local.set $x) (if (local.get $x) (then (return (local.get $x)))) (br $loop) + ) (; ADD: ;) (call $mpop1) (call $wpop1) (i32.add) (call $wpsh1) (br $loop) + ) (; SUB: ;) (call $mpop1) (local.set $x) (call $wpop1) (local.get $x) (i32.sub) (call $wpsh1) (br $loop) + ) (; INC: ;) (call $mpop1) (i32.add (i32.const 1)) (call $wpsh1) (br $loop) + ) (; DEC: ;) (call $mpop1) (i32.sub (i32.const 1)) (call $wpsh1) (br $loop) + ) (; LTH: ;) (call $mpop1) (call $wpop1) (i32.gt_u) (call $wpshb) (br $loop) + ) (; GTH: ;) (call $mpop1) (call $wpop1) (i32.lt_u) (call $wpshb) (br $loop) + ) (; EQU: ;) (call $mpop1) (call $wpop1) (i32.eq) (call $wpshb) (br $loop) + ) (; NQK: ;) (call $mpop1) (local.tee $x) (call $wget1) (local.get $x) (call $wpsh1) (i32.ne) (call $wpshb) (br $loop) + ) (; SHL: ;) (call $mpop1) (local.set $x) (call $wpop1) (local.get $x) (i32.shl) (call $wpsh1) (br $loop) + ) (; SHR: ;) (call $mpop1) (local.set $x) (call $wpop1) (local.get $x) (i32.shr_u) (call $wpsh1) (br $loop) + ) (; ROL: ;) (call $mpop1) (local.set $x) (call $wpop1) (local.get $x) (call $rol1) (call $wpsh1) (br $loop) + ) (; ROR: ;) (call $mpop1) (local.set $x) (call $wpop1) (local.get $x) (call $ror1) (call $wpsh1) (br $loop) + ) (; IOR: ;) (call $mpop1) (call $wpop1) (i32.or) (call $wpsh1) (br $loop) + ) (; XOR: ;) (call $mpop1) (call $wpop1) (i32.xor) (call $wpsh1) (br $loop) + ) (; AND: ;) (call $mpop1) (call $wpop1) (i32.and) (call $wpsh1) (br $loop) + ) (; NOT: ;) (call $mpop1) (i32.xor (i32.const 0xFF)) (call $wpsh1) (br $loop) + + ) (; DB1* ;) (return (global.get $SIGNAL.DB1)) + ) (; PSH* ;) (call $rpop2) (call $wpsh2) (br $loop) + ) (; POP* ;) (call $wpmov (i32.const -2)) (br $loop) + ) (; CPY* ;) (call $rget2) (call $wpsh2) (br $loop) + ) (; DUP* ;) (call $wget2) (call $wpsh2) (br $loop) + ) (; OVR* ;) (call $wpop2) (call $wget2) (local.set $x) (call $wpsh2) (local.get $x) (call $wpsh2) (br $loop) + ) (; SWP* ;) (call $wpop2) (call $wpop2) (local.set $x) (call $wpsh2) (local.get $x) (call $wpsh2) (br $loop) + ) (; ROT* ;) (call $wpop2) (call $wpop2) (call $wpop2) (local.set $x) (call $wpsh2) (call $wpsh2) (local.get $x) (call $wpsh2) (br $loop) + ) (; JMP* ;) (call $wpop2) (global.set $ip) (br $loop) + ) (; JMS* ;) (call $wpop2) (global.get $ip) (call $rpsh2) (global.set $ip) (br $loop) + ) (; JCN* ;) (call $wpop2) (local.set $x) (if (call $wpop2) (then (local.get $x) (global.set $ip))) (br $loop) + ) (; JCS* ;) (call $wpop2) (local.set $x) (if (call $wpop2) (then (global.get $ip) (call $rpsh2) (local.get $x) (global.set $ip))) (br $loop) + ) (; LDA* ;) (call $wpop2) (call $mget2) (call $wpsh2) (br $loop) + ) (; STA* ;) (call $wpop2) (call $wpop2) (call $mset2) (br $loop) + ) (; LDD* ;) (call $wpop1) (call $dget2) (call $wpsh2) (br $loop) + ) (; STD* ;) (call $wpop1) (call $wpop2) (call $dset2) (local.set $x) (if (local.get $x) (then (return (local.get $x)))) (br $loop) + ) (; ADD* ;) (call $wpop2) (call $wpop2) (i32.add) (call $wpsh2) (br $loop) + ) (; SUB* ;) (call $wpop2) (local.set $x) (call $wpop2) (local.get $x) (i32.sub) (call $wpsh2) (br $loop) + ) (; INC* ;) (call $wpop2) (i32.add (i32.const 1)) (call $wpsh2) (br $loop) + ) (; DEC* ;) (call $wpop2) (i32.sub (i32.const 1)) (call $wpsh2) (br $loop) + ) (; LTH* ;) (call $wpop2) (call $wpop2) (i32.gt_u) (call $wpshb) (br $loop) + ) (; GTH* ;) (call $wpop2) (call $wpop2) (i32.lt_u) (call $wpshb) (br $loop) + ) (; EQU* ;) (call $wpop2) (call $wpop2) (i32.eq) (call $wpshb) (br $loop) + ) (; NQK* ;) (call $wpop2) (local.tee $x) (call $wget2) (local.get $x) (call $wpsh2) (i32.ne) (call $wpshb) (br $loop) + ) (; SHL* ;) (call $wpop1) (local.set $x) (call $wpop2) (local.get $x) (i32.shl) (call $wpsh2) (br $loop) + ) (; SHR* ;) (call $wpop1) (local.set $x) (call $wpop2) (local.get $x) (i32.shr_u) (call $wpsh2) (br $loop) + ) (; ROL* ;) (call $wpop1) (local.set $x) (call $wpop2) (local.get $x) (call $rol2) (call $wpsh2) (br $loop) + ) (; ROR* ;) (call $wpop1) (local.set $x) (call $wpop2) (local.get $x) (call $ror2) (call $wpsh2) (br $loop) + ) (; IOR* ;) (call $wpop2) (call $wpop2) (i32.or) (call $wpsh2) (br $loop) + ) (; XOR* ;) (call $wpop2) (call $wpop2) (i32.xor) (call $wpsh2) (br $loop) + ) (; AND* ;) (call $wpop2) (call $wpop2) (i32.and) (call $wpsh2) (br $loop) + ) (; NOT* ;) (call $wpop2) (i32.xor (i32.const 0xFFFF)) (call $wpsh2) (br $loop) + + ) (; DB2 ;) (return (global.get $SIGNAL.DB2)) + ) (; PSH*: ;) (call $mpop2) (call $wpsh2) (br $loop) + ) (; POP*: ;) (call $ipmov (i32.const 2)) (br $loop) + ) (; CPY*: ;) (call $mpop2) (local.tee $x) (call $wpsh2) (local.get $x) (call $rpsh2) (br $loop) + ) (; DUP*: ;) (call $mpop2) (local.tee $x) (call $wpsh2) (local.get $x) (call $wpsh2) (br $loop) + ) (; OVR*: ;) (call $mpop2) (call $wget2) (local.set $x) (call $wpsh2) (local.get $x) (call $wpsh2) (br $loop) + ) (; SWP*: ;) (call $mpop2) (call $wpop2) (local.set $x) (call $wpsh2) (local.get $x) (call $wpsh2) (br $loop) + ) (; ROT*: ;) (call $mpop2) (call $wpop2) (call $wpop2) (local.set $x) (call $wpsh2) (call $wpsh2) (local.get $x) (call $wpsh2) (br $loop) + ) (; JMP*: ;) (call $mpop2) (global.set $ip) (br $loop) + ) (; JMS*: ;) (call $mpop2) (global.get $ip) (call $rpsh2) (global.set $ip) (br $loop) + ) (; JCN*: ;) (call $mpop2) (local.set $x) (if (call $wpop2) (then (local.get $x) (global.set $ip))) (br $loop) + ) (; JCS*: ;) (call $mpop2) (local.set $x) (if (call $wpop2) (then (global.get $ip) (call $rpsh2) (local.get $x) (global.set $ip))) (br $loop) + ) (; LDA*: ;) (call $mpop2) (call $mget2) (call $wpsh2) (br $loop) + ) (; STA*: ;) (call $mpop2) (call $wpop2) (call $mset2) (br $loop) + ) (; LDD*: ;) (call $mpop1) (call $dget2) (call $wpsh2) (br $loop) + ) (; STD*: ;) (call $mpop1) (call $wpop2) (call $dset2) (local.set $x) (if (local.get $x) (then (return (local.get $x)))) (br $loop) + ) (; ADD*: ;) (call $mpop2) (call $wpop2) (i32.add) (call $wpsh2) (br $loop) + ) (; SUB*: ;) (call $mpop2) (local.set $x) (call $wpop2) (local.get $x) (i32.sub) (call $wpsh2) (br $loop) + ) (; INC*: ;) (call $mpop2) (i32.add (i32.const 1)) (call $wpsh2) (br $loop) + ) (; DEC*: ;) (call $mpop2) (i32.sub (i32.const 1)) (call $wpsh2) (br $loop) + ) (; LTH*: ;) (call $mpop2) (call $wpop2) (i32.gt_u) (call $wpshb) (br $loop) + ) (; GTH*: ;) (call $mpop2) (call $wpop2) (i32.lt_u) (call $wpshb) (br $loop) + ) (; EQU*: ;) (call $mpop2) (call $wpop2) (i32.eq) (call $wpshb) (br $loop) + ) (; NQK*: ;) (call $mpop2) (local.tee $x) (call $wget2) (local.get $x) (call $wpsh2) (i32.ne) (call $wpshb) (br $loop) + ) (; SHL*: ;) (call $mpop1) (local.set $x) (call $wpop2) (local.get $x) (i32.shl) (call $wpsh2) (br $loop) + ) (; SHR*: ;) (call $mpop1) (local.set $x) (call $wpop2) (local.get $x) (i32.shr_u) (call $wpsh2) (br $loop) + ) (; ROL*: ;) (call $mpop1) (local.set $x) (call $wpop2) (local.get $x) (call $rol2) (call $wpsh2) (br $loop) + ) (; ROR*: ;) (call $mpop1) (local.set $x) (call $wpop2) (local.get $x) (call $ror2) (call $wpsh2) (br $loop) + ) (; IOR*: ;) (call $mpop2) (call $wpop2) (i32.or) (call $wpsh2) (br $loop) + ) (; XOR*: ;) (call $mpop2) (call $wpop2) (i32.xor) (call $wpsh2) (br $loop) + ) (; AND*: ;) (call $mpop2) (call $wpop2) (i32.and) (call $wpsh2) (br $loop) + ) (; NOT*: ;) (call $mpop2) (i32.xor (i32.const 0xFFFF)) (call $wpsh2) (br $loop) + + ) (; DB3 ;) (return (global.get $SIGNAL.DB3)) + ) (; PSHr ;) (call $wpop1) (call $rpsh1) (br $loop) + ) (; POPr ;) (call $rpmov (i32.const -1)) (br $loop) + ) (; CPYr ;) (call $wget1) (call $rpsh1) (br $loop) + ) (; DUPr ;) (call $rget1) (call $rpsh1) (br $loop) + ) (; OVRr ;) (call $rpop1) (call $rget1) (local.set $x) (call $rpsh1) (local.get $x) (call $rpsh1) (br $loop) + ) (; SWPr ;) (call $rpop1) (call $rpop1) (local.set $x) (call $rpsh1) (local.get $x) (call $rpsh1) (br $loop) + ) (; ROTr ;) (call $rpop1) (call $rpop1) (call $rpop1) (local.set $x) (call $rpsh1) (call $rpsh1) (local.get $x) (call $rpsh1) (br $loop) + ) (; JMPr ;) (call $rpop2) (global.set $ip) (br $loop) + ) (; JMSr ;) (call $rpop2) (global.get $ip) (call $wpsh2) (global.set $ip) (br $loop) + ) (; JCNr ;) (call $rpop2) (local.set $x) (if (call $rpop1) (then (local.get $x) (global.set $ip))) (br $loop) + ) (; JCSr ;) (call $rpop2) (local.set $x) (if (call $rpop1) (then (global.get $ip) (call $wpsh2) (local.get $x) (global.set $ip))) (br $loop) + ) (; LDAr ;) (call $rpop2) (call $mget1) (call $rpsh1) (br $loop) + ) (; STAr ;) (call $rpop2) (call $rpop1) (call $mset1) (br $loop) + ) (; LDDr ;) (call $rpop1) (call $dget1) (call $rpsh1) (br $loop) + ) (; STDr ;) (call $rpop1) (call $rpop1) (call $dset1) (local.set $x) (if (local.get $x) (then (return (local.get $x)))) (br $loop) + ) (; ADDr ;) (call $rpop1) (call $rpop1) (i32.add) (call $rpsh1) (br $loop) + ) (; SUBr ;) (call $rpop1) (local.set $x) (call $rpop1) (local.get $x) (i32.sub) (call $rpsh1) (br $loop) + ) (; INCr ;) (call $rpop1) (i32.add (i32.const 1)) (call $rpsh1) (br $loop) + ) (; DECr ;) (call $rpop1) (i32.sub (i32.const 1)) (call $rpsh1) (br $loop) + ) (; LTHr ;) (call $rpop1) (call $rpop1) (i32.gt_u) (call $rpshb) (br $loop) + ) (; GTHr ;) (call $rpop1) (call $rpop1) (i32.lt_u) (call $rpshb) (br $loop) + ) (; EQUr ;) (call $rpop1) (call $rpop1) (i32.eq) (call $rpshb) (br $loop) + ) (; NQKr ;) (call $rpop1) (local.tee $x) (call $rget1) (local.get $x) (call $rpsh1) (i32.ne) (call $rpshb) (br $loop) + ) (; SHLr ;) (call $rpop1) (local.set $x) (call $rpop1) (local.get $x) (i32.shl) (call $rpsh1) (br $loop) + ) (; SHRr ;) (call $rpop1) (local.set $x) (call $rpop1) (local.get $x) (i32.shr_u) (call $rpsh1) (br $loop) + ) (; ROLr ;) (call $rpop1) (local.set $x) (call $rpop1) (local.get $x) (call $rol1) (call $rpsh1) (br $loop) + ) (; RORr ;) (call $rpop1) (local.set $x) (call $rpop1) (local.get $x) (call $ror1) (call $rpsh1) (br $loop) + ) (; IORr ;) (call $rpop1) (call $rpop1) (i32.or) (call $rpsh1) (br $loop) + ) (; XORr ;) (call $rpop1) (call $rpop1) (i32.xor) (call $rpsh1) (br $loop) + ) (; ANDr ;) (call $rpop1) (call $rpop1) (i32.and) (call $rpsh1) (br $loop) + ) (; NOTr ;) (call $rpop1) (i32.xor (i32.const 0xFF)) (call $rpsh1) (br $loop) + + ) (; DB4 ;) (return (global.get $SIGNAL.DB4)) + ) (; PSHr: ;) (call $mpop1) (call $rpsh1) (br $loop) + ) (; POPr: ;) (call $ipmov (i32.const 1)) (br $loop) + ) (; CPYr: ;) (call $mpop1) (local.tee $x) (call $rpsh1) (local.get $x) (call $wpsh1) (br $loop) + ) (; DUPr: ;) (call $mpop1) (local.tee $x) (call $rpsh1) (local.get $x) (call $rpsh1) (br $loop) + ) (; OVRr: ;) (call $mpop1) (call $rget1) (local.set $x) (call $rpsh1) (local.get $x) (call $rpsh1) (br $loop) + ) (; SWPr: ;) (call $mpop1) (call $rpop1) (local.set $x) (call $rpsh1) (local.get $x) (call $rpsh1) (br $loop) + ) (; ROTr: ;) (call $mpop1) (call $rpop1) (call $rpop1) (local.set $x) (call $rpsh1) (call $rpsh1) (local.get $x) (call $rpsh1) (br $loop) + ) (; JMPr: ;) (call $mpop2) (global.set $ip) (br $loop) + ) (; JMSr: ;) (call $mpop2) (global.get $ip) (call $wpsh2) (global.set $ip) (br $loop) + ) (; JCNr: ;) (call $mpop2) (local.set $x) (if (call $rpop1) (then (local.get $x) (global.set $ip))) (br $loop) + ) (; JCSr: ;) (call $mpop2) (local.set $x) (if (call $rpop1) (then (global.get $ip) (call $wpsh2) (local.get $x) (global.set $ip))) (br $loop) + ) (; LDAr: ;) (call $mpop2) (call $mget1) (call $rpsh1) (br $loop) + ) (; STAr: ;) (call $mpop2) (call $rpop1) (call $mset1) (br $loop) + ) (; LDDr: ;) (call $mpop1) (call $dget1) (call $rpsh1) (br $loop) + ) (; STDr: ;) (call $mpop1) (call $rpop1) (call $dset1) (local.set $x) (if (local.get $x) (then (return (local.get $x)))) (br $loop) + ) (; ADDr: ;) (call $mpop1) (call $rpop1) (i32.add) (call $rpsh1) (br $loop) + ) (; SUBr: ;) (call $mpop1) (local.set $x) (call $rpop1) (local.get $x) (i32.sub) (call $rpsh1) (br $loop) + ) (; INCr: ;) (call $mpop1) (i32.add (i32.const 1)) (call $rpsh1) (br $loop) + ) (; DECr: ;) (call $mpop1) (i32.sub (i32.const 1)) (call $rpsh1) (br $loop) + ) (; LTHr: ;) (call $mpop1) (call $rpop1) (i32.gt_u) (call $rpshb) (br $loop) + ) (; GTHr: ;) (call $mpop1) (call $rpop1) (i32.lt_u) (call $rpshb) (br $loop) + ) (; EQUr: ;) (call $mpop1) (call $rpop1) (i32.eq) (call $rpshb) (br $loop) + ) (; NQKr: ;) (call $mpop1) (local.tee $x) (call $rget1) (local.get $x) (call $rpsh1) (i32.ne) (call $rpshb) (br $loop) + ) (; SHLr: ;) (call $mpop1) (local.set $x) (call $rpop1) (local.get $x) (i32.shl) (call $rpsh1) (br $loop) + ) (; SHRr: ;) (call $mpop1) (local.set $x) (call $rpop1) (local.get $x) (i32.shr_u) (call $rpsh1) (br $loop) + ) (; ROLr: ;) (call $mpop1) (local.set $x) (call $rpop1) (local.get $x) (call $rol1) (call $rpsh1) (br $loop) + ) (; RORr: ;) (call $mpop1) (local.set $x) (call $rpop1) (local.get $x) (call $ror1) (call $rpsh1) (br $loop) + ) (; IORr: ;) (call $mpop1) (call $rpop1) (i32.or) (call $rpsh1) (br $loop) + ) (; XORr: ;) (call $mpop1) (call $rpop1) (i32.xor) (call $rpsh1) (br $loop) + ) (; ANDr: ;) (call $mpop1) (call $rpop1) (i32.and) (call $rpsh1) (br $loop) + ) (; NOTr: ;) (call $mpop1) (i32.xor (i32.const 0xFF)) (call $rpsh1) (br $loop) + + ) (; DB5 ;) (return (global.get $SIGNAL.DB5)) + ) (; PSHr* ;) (call $wpop2) (call $rpsh2) (br $loop) + ) (; POPr* ;) (call $rpmov (i32.const -2)) (br $loop) + ) (; CPYr* ;) (call $wget2) (call $rpsh2) (br $loop) + ) (; DUPr* ;) (call $rget2) (call $rpsh2) (br $loop) + ) (; OVRr* ;) (call $rpop2) (call $rget2) (local.set $x) (call $rpsh2) (local.get $x) (call $rpsh2) (br $loop) + ) (; SWPr* ;) (call $rpop2) (call $rpop2) (local.set $x) (call $rpsh2) (local.get $x) (call $rpsh2) (br $loop) + ) (; ROTr* ;) (call $rpop2) (call $rpop2) (call $rpop2) (local.set $x) (call $rpsh2) (call $rpsh2) (local.get $x) (call $rpsh2) (br $loop) + ) (; JMPr* ;) (call $rpop2) (global.set $ip) (br $loop) + ) (; JMSr* ;) (call $rpop2) (global.get $ip) (call $wpsh2) (global.set $ip) (br $loop) + ) (; JCNr* ;) (call $rpop2) (local.set $x) (if (call $rpop2) (then (local.get $x) (global.set $ip))) (br $loop) + ) (; JCSr* ;) (call $rpop2) (local.set $x) (if (call $rpop2) (then (global.get $ip) (call $wpsh2) (local.get $x) (global.set $ip))) (br $loop) + ) (; LDAr* ;) (call $rpop2) (call $mget2) (call $rpsh2) (br $loop) + ) (; STAr* ;) (call $rpop2) (call $rpop2) (call $mset2) (br $loop) + ) (; LDDr* ;) (call $rpop1) (call $dget2) (call $rpsh2) (br $loop) + ) (; STDr* ;) (call $rpop1) (call $rpop2) (call $dset2) (local.set $x) (if (local.get $x) (then (return (local.get $x)))) (br $loop) + ) (; ADDr* ;) (call $rpop2) (call $rpop2) (i32.add) (call $rpsh2) (br $loop) + ) (; SUBr* ;) (call $rpop2) (local.set $x) (call $rpop2) (local.get $x) (i32.sub) (call $rpsh2) (br $loop) + ) (; INCr* ;) (call $rpop2) (i32.add (i32.const 1)) (call $rpsh2) (br $loop) + ) (; DECr* ;) (call $rpop2) (i32.sub (i32.const 1)) (call $rpsh2) (br $loop) + ) (; LTHr* ;) (call $rpop2) (call $rpop2) (i32.gt_u) (call $rpshb) (br $loop) + ) (; GTHr* ;) (call $rpop2) (call $rpop2) (i32.lt_u) (call $rpshb) (br $loop) + ) (; EQUr* ;) (call $rpop2) (call $rpop2) (i32.eq) (call $rpshb) (br $loop) + ) (; NQKr* ;) (call $rpop2) (local.tee $x) (call $rget2) (local.get $x) (call $rpsh2) (i32.ne) (call $rpshb) (br $loop) + ) (; SHLr* ;) (call $rpop1) (local.set $x) (call $rpop2) (local.get $x) (i32.shl) (call $rpsh2) (br $loop) + ) (; SHRr* ;) (call $rpop1) (local.set $x) (call $rpop2) (local.get $x) (i32.shr_u) (call $rpsh2) (br $loop) + ) (; ROLr* ;) (call $rpop1) (local.set $x) (call $rpop2) (local.get $x) (call $rol2) (call $rpsh2) (br $loop) + ) (; RORr* ;) (call $rpop1) (local.set $x) (call $rpop2) (local.get $x) (call $ror2) (call $rpsh2) (br $loop) + ) (; IORr* ;) (call $rpop2) (call $rpop2) (i32.or) (call $rpsh2) (br $loop) + ) (; XORr* ;) (call $rpop2) (call $rpop2) (i32.xor) (call $rpsh2) (br $loop) + ) (; ANDr* ;) (call $rpop2) (call $rpop2) (i32.and) (call $rpsh2) (br $loop) + ) (; NOTr* ;) (call $rpop2) (i32.xor (i32.const 0xFFFF)) (call $rpsh2) (br $loop) + + ) (; DB6 ;) (return (global.get $SIGNAL.DB6)) + ) (; PSHr*: ;) (call $mpop2) (call $rpsh2) (br $loop) + ) (; POPr*: ;) (call $ipmov (i32.const 2)) (br $loop) + ) (; CPYr*: ;) (call $mpop2) (local.tee $x) (call $rpsh2) (local.get $x) (call $wpsh2) (br $loop) + ) (; DUPr*: ;) (call $mpop2) (local.tee $x) (call $rpsh2) (local.get $x) (call $rpsh2) (br $loop) + ) (; OVRr*: ;) (call $mpop2) (call $rget2) (local.set $x) (call $rpsh2) (local.get $x) (call $rpsh2) (br $loop) + ) (; SWPr*: ;) (call $mpop2) (call $rpop2) (local.set $x) (call $rpsh2) (local.get $x) (call $rpsh2) (br $loop) + ) (; ROTr*: ;) (call $mpop2) (call $rpop2) (call $rpop2) (local.set $x) (call $rpsh2) (call $rpsh2) (local.get $x) (call $rpsh2) (br $loop) + ) (; JMPr*: ;) (call $mpop2) (global.set $ip) (br $loop) + ) (; JMSr*: ;) (call $mpop2) (global.get $ip) (call $wpsh2) (global.set $ip) (br $loop) + ) (; JCNr*: ;) (call $mpop2) (local.set $x) (if (call $rpop2) (then (local.get $x) (global.set $ip))) (br $loop) + ) (; JCSr*: ;) (call $mpop2) (local.set $x) (if (call $rpop2) (then (global.get $ip) (call $wpsh2) (local.get $x) (global.set $ip))) (br $loop) + ) (; LDAr*: ;) (call $mpop2) (call $mget2) (call $rpsh2) (br $loop) + ) (; STAr*: ;) (call $mpop2) (call $rpop2) (call $mset2) (br $loop) + ) (; LDDr*: ;) (call $mpop1) (call $dget2) (call $rpsh2) (br $loop) + ) (; STDr*: ;) (call $mpop1) (call $rpop2) (call $dset2) (local.set $x) (if (local.get $x) (then (return (local.get $x)))) (br $loop) + ) (; ADDr*: ;) (call $mpop2) (call $rpop2) (i32.add) (call $rpsh2) (br $loop) + ) (; SUBr*: ;) (call $mpop2) (local.set $x) (call $rpop2) (local.get $x) (i32.sub) (call $rpsh2) (br $loop) + ) (; INCr*: ;) (call $mpop2) (i32.add (i32.const 1)) (call $rpsh2) (br $loop) + ) (; DECr*: ;) (call $mpop2) (i32.sub (i32.const 1)) (call $rpsh2) (br $loop) + ) (; LTHr*: ;) (call $mpop2) (call $rpop2) (i32.gt_u) (call $rpshb) (br $loop) + ) (; GTHr*: ;) (call $mpop2) (call $rpop2) (i32.lt_u) (call $rpshb) (br $loop) + ) (; EQUr*: ;) (call $mpop2) (call $rpop2) (i32.eq) (call $rpshb) (br $loop) + ) (; NQKr*: ;) (call $mpop2) (local.tee $x) (call $rget2) (local.get $x) (call $rpsh2) (i32.ne) (call $rpshb) (br $loop) + ) (; SHLr*: ;) (call $mpop1) (local.set $x) (call $rpop2) (local.get $x) (i32.shl) (call $rpsh2) (br $loop) + ) (; SHRr*: ;) (call $mpop1) (local.set $x) (call $rpop2) (local.get $x) (i32.shr_u) (call $rpsh2) (br $loop) + ) (; ROLr*: ;) (call $mpop1) (local.set $x) (call $rpop2) (local.get $x) (call $rol2) (call $rpsh2) (br $loop) + ) (; RORr*: ;) (call $mpop1) (local.set $x) (call $rpop2) (local.get $x) (call $ror2) (call $rpsh2) (br $loop) + ) (; IORr*: ;) (call $mpop2) (call $rpop2) (i32.or) (call $rpsh2) (br $loop) + ) (; XORr*: ;) (call $mpop2) (call $rpop2) (i32.xor) (call $rpsh2) (br $loop) + ) (; ANDr*: ;) (call $mpop2) (call $rpop2) (i32.and) (call $rpsh2) (br $loop) + ) (; NOTr*: ;) (call $mpop2) (i32.xor (i32.const 0xFFFF)) (call $rpsh2) (br $loop) + ) + ) (; break ;) + + ;; Default return signal. + (return (global.get $SIGNAL.BREAK)) + ) +) @@ -3,52 +3,138 @@ let systemName = "bedrock-js"; let systemVersion = "1.0.1"; let systemAuthors = "Ben Bridle"; -let initialScreenScale = 2; +let defaultToWasm = true; // default to using WASM core for emulators +let maxFrameTime = 500; // milliseconds before a render is forced +let initialScreenScale = 2; // screen pixel scale +let tabSize = 2; // width of tabs in assembler +let maxTransmissions = 100; // maximum number of transmissions to retain -// Upgrade every pre.bedrock element to a full assembler, and every -// bedrock to a full emulator. -window.addEventListener('DOMContentLoaded', function() { - injectStyles(); +// Number of emulator cycles to run between updates. This value massively affects +// performance: if it's too high, the UI will lag, and if it's too low, the +// emulator will run slow. We must take the middle way. +// TODO: Find a way to dynamically calculate the optimal value at runtime, +// based on time taken to run each batch. +let cyclesPerBatch = defaultToWasm ? 800000 : 50000 ; + +// ----------------------------------------------------------------------------------------------- + +// :::::: WEBASSEMBLY MODULE :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + +// ----------------------------------------------------------------------------------------------- + + + +// WebAssembly module bytecode embedded as a base64 string. +let wasmBytecode = Uint8Array.from(atob( +'AGFzbQEAAAABIAdgAX8Bf2ACf38Bf2AAAGAAAX5gAAF/YAF/AGACf38AAiECB2JlZHJvY2sFZGdldDEAAAdiZWRyb2NrBWRzZXQxAAEDJCMCAwQEBAUFBQUFBQQEBAQEBAQEBAQAAAAGBgEFBQUBAQEB' + +'AAUDAQACBmMSfgFCAAt/AUEAC38BQYCCBAt/AUGAhAQLfwBBAAt/AEGAggQLfwBBgIQEC38AQQALfwBBAQt/AEECC38AQQMLfwBBBAt/AEEFC38AQQYLfwBBBwt/AEEIC38AQQkLfwBBCgsHkQIkBm1l' + +'bW9yeQIABXJlc2V0AAICY2MAAwJpcAAEAndwAAUCcnAABgV3cHNoMQAHBXdwc2gyAAgFcnBzaDEACQVycHNoMgAKBXdwc2hiAAsFcnBzaGIADAV3cG9wMQANBXdwb3AyAA4FcnBvcDEADwVycG9wMgAQ' + +'BW1wb3AxABEFbXBvcDIAEgV3Z2V0MQATBXdnZXQyABQFcmdldDEAFQVyZ2V0MgAWBW1nZXQxABcFbWdldDIAGAVkZ2V0MgAZBW1zZXQxABoFbXNldDIAGwVkc2V0MgAcBWlwbW92AB0Fd3Btb3YAHgVy' + +'cG1vdgAfBHJvbDEAIARyb3IxACEEcm9sMgAiBHJvcjIAIwRldmFsACQK6CMjDgAjBCQBIwUkAiMGJAMLBAAjAAsEACMBCwcAIwUjAmsLBwAjBiMDawsQACMCQQFrJAIjAiAAOgAACxAAIwJBAmskAiMC' + +'IAA7AQALEAAjA0EBayQDIwMgADoAAAsQACMDQQJrJAMjAyAAOwEACwwAQf8BQQAgABsQBwsMAEH/AUEAIAAbEAkLDgAjAi0AACMCQQFqJAILDgAjAi8BACMCQQJqJAILDgAjAy0AACMDQQFqJAMLDgAj' + +'Ay8BACMDQQJqJAMLDgAjAS0AACMBQQFqJAELGgAjAS0AAEEIdCMBQQFqLQAAciMBQQJqJAELBwAjAi0AAAsHACMCLwEACwcAIwMtAAALBwAjAy8BAAsHACAALQAACxMAIAAtAABBCHQgAEEBai0AAHIL' + +'EQAgABAAQQh0IABBAWoQAHILCQAgACABOgAACxoAIAAgAUEIdjoAACAAQQFqIAFB/wFxOgAACxkAIAAgAUEIdhABIABBAWogAUH/AXEQAXILCQAjASAAaiQBCwkAIwIgAGskAgsJACMDIABrJAMLFwAg' + +'AUEHcSEBIAAgAXQgAEEIIAFrdnILFwAgAUEHcSEBIABBCCABa3QgACABdnILFwAgAUEPcSEBIAAgAXQgAEEQIAFrdnILFwAgAUEPcSEBIABBECABa3QgACABdnIL7R8CAX4BfyMAIACtfCEBA0ACQCAB' + +'IwBRBEAMAQsjAEIBfCQAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkAC' + +'QAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkAC' + +'QAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkAC' + +'QAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkAC' + +'QAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkAQEQ7/AQABAgMEBQYHCAkKCwwNDg8QERITFBUWFxgZGhscHR4fICEiIyQl' + +'JicoKSorLC0uLzAxMjM0NTY3ODk6Ozw9Pj9AQUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVpbXF1eX2BhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ent8fX5/gAGBAYIBgwGEAYUBhgGHAYgBiQGKAYsB' + +'jAGNAY4BjwGQAZEBkgGTAZQBlQGWAZcBmAGZAZoBmwGcAZ0BngGfAaABoQGiAaMBpAGlAaYBpwGoAakBqgGrAawBrQGuAa8BsAGxAbIBswG0AbUBtgG3AbgBuQG6AbsBvAG9Ab4BvwHAAcEBwgHDAcQB' + +'xQHGAccByAHJAcoBywHMAc0BzgHPAdAB0QHSAdMB1AHVAdYB1wHYAdkB2gHbAdwB3QHeAd8B4AHhAeIB4wHkAeUB5gHnAegB6QHqAesB7AHtAe4B7wHwAfEB8gHzAfQB9QH2AfcB+AH5AfoB+wH8Af0B' + +'/gH/AQsjCA8LEA8QBwz/AQtBfxAeDP4BCxAVEAcM/QELEBMQBwz8AQsQDRATIQIQByACEAcM+wELEA0QDSECEAcgAhAHDPoBCxANEA0QDSECEAcQByACEAcM+QELEA4kAQz4AQsQDiMBEAokAQz3AQsQ' + +'DiECEA0EQCACJAELDPYBCxAOIQIQDQRAIwEQCiACJAELDPUBCxAOEBcQBwz0AQsQDhANEBoM8wELEA0QABAHDPIBCxANEA0QASECIAIEQCACDwsM8QELEA0QDWoQBwzwAQsQDSECEA0gAmsQBwzvAQsQ' + +'DUEBahAHDO4BCxANQQFrEAcM7QELEA0QDUsQCwzsAQsQDRANSRALDOsBCxANEA1GEAsM6gELEA0iAhATIAIQB0cQCwzpAQsQDSECEA0gAnQQBwzoAQsQDSECEA0gAnYQBwznAQsQDSECEA0gAhAgEAcM' + +'5gELEA0hAhANIAIQIRAHDOUBCxANEA1yEAcM5AELEA0QDXMQBwzjAQsQDRANcRAHDOIBCxANQf8BcxAHDOEBCwzgAQsQERAHDN8BC0EBEB0M3gELEBEiAhAHIAIQCQzdAQsQESICEAcgAhAHDNwBCxAR' + +'EBMhAhAHIAIQBwzbAQsQERANIQIQByACEAcM2gELEBEQDRANIQIQBxAHIAIQBwzZAQsQEiQBDNgBCxASIwEQCiQBDNcBCxASIQIQDQRAIAIkAQsM1gELEBIhAhANBEAjARAKIAIkAQsM1QELEBIQFxAH' + +'DNQBCxASEA0QGgzTAQsQERAAEAcM0gELEBEQDRABIQIgAgRAIAIPCwzRAQsQERANahAHDNABCxARIQIQDSACaxAHDM8BCxARQQFqEAcMzgELEBFBAWsQBwzNAQsQERANSxALDMwBCxAREA1JEAsMywEL' + +'EBEQDUYQCwzKAQsQESICEBMgAhAHRxALDMkBCxARIQIQDSACdBAHDMgBCxARIQIQDSACdhAHDMcBCxARIQIQDSACECAQBwzGAQsQESECEA0gAhAhEAcMxQELEBEQDXIQBwzEAQsQERANcxAHDMMBCxAR' + +'EA1xEAcMwgELEBFB/wFzEAcMwQELIwkPCxAQEAgMvwELQX4QHgy+AQsQFhAIDL0BCxAUEAgMvAELEA4QFCECEAggAhAIDLsBCxAOEA4hAhAIIAIQCAy6AQsQDhAOEA4hAhAIEAggAhAIDLkBCxAOJAEM' + +'uAELEA4jARAKJAEMtwELEA4hAhAOBEAgAiQBCwy2AQsQDiECEA4EQCMBEAogAiQBCwy1AQsQDhAYEAgMtAELEA4QDhAbDLMBCxANEBkQCAyyAQsQDRAOEBwhAiACBEAgAg8LDLEBCxAOEA5qEAgMsAEL' + +'EA4hAhAOIAJrEAgMrwELEA5BAWoQCAyuAQsQDkEBaxAIDK0BCxAOEA5LEAsMrAELEA4QDkkQCwyrAQsQDhAORhALDKoBCxAOIgIQFCACEAhHEAsMqQELEA0hAhAOIAJ0EAgMqAELEA0hAhAOIAJ2EAgM' + +'pwELEA0hAhAOIAIQIhAIDKYBCxANIQIQDiACECMQCAylAQsQDhAOchAIDKQBCxAOEA5zEAgMowELEA4QDnEQCAyiAQsQDkH//wNzEAgMoQELIwoPCxASEAgMnwELQQIQHQyeAQsQEiICEAggAhAKDJ0B' + +'CxASIgIQCCACEAgMnAELEBIQFCECEAggAhAIDJsBCxASEA4hAhAIIAIQCAyaAQsQEhAOEA4hAhAIEAggAhAIDJkBCxASJAEMmAELEBIjARAKJAEMlwELEBIhAhAOBEAgAiQBCwyWAQsQEiECEA4EQCMB' + +'EAogAiQBCwyVAQsQEhAYEAgMlAELEBIQDhAbDJMBCxAREBkQCAySAQsQERAOEBwhAiACBEAgAg8LDJEBCxASEA5qEAgMkAELEBIhAhAOIAJrEAgMjwELEBJBAWoQCAyOAQsQEkEBaxAIDI0BCxASEA5L' + +'EAsMjAELEBIQDkkQCwyLAQsQEhAORhALDIoBCxASIgIQFCACEAhHEAsMiQELEBEhAhAOIAJ0EAgMiAELEBEhAhAOIAJ2EAgMhwELEBEhAhAOIAIQIhAIDIYBCxARIQIQDiACECMQCAyFAQsQEhAOchAI' + +'DIQBCxASEA5zEAgMgwELEBIQDnEQCAyCAQsQEkH//wNzEAgMgQELIwsPCxANEAkMfwtBfxAfDH4LEBMQCQx9CxAVEAkMfAsQDxAVIQIQCSACEAkMewsQDxAPIQIQCSACEAkMegsQDxAPEA8hAhAJEAkg' + +'AhAJDHkLEBAkAQx4CxAQIwEQCCQBDHcLEBAhAhAPBEAgAiQBCwx2CxAQIQIQDwRAIwEQCCACJAELDHULEBAQFxAJDHQLEBAQDxAaDHMLEA8QABAJDHILEA8QDxABIQIgAgRAIAIPCwxxCxAPEA9qEAkM' + +'cAsQDyECEA8gAmsQCQxvCxAPQQFqEAkMbgsQD0EBaxAJDG0LEA8QD0sQDAxsCxAPEA9JEAwMawsQDxAPRhAMDGoLEA8iAhAVIAIQCUcQDAxpCxAPIQIQDyACdBAJDGgLEA8hAhAPIAJ2EAkMZwsQDyEC' + +'EA8gAhAgEAkMZgsQDyECEA8gAhAhEAkMZQsQDxAPchAJDGQLEA8QD3MQCQxjCxAPEA9xEAkMYgsQD0H/AXMQCQxhCyMMDwsQERAJDF8LQQEQHQxeCxARIgIQCSACEAcMXQsQESICEAkgAhAJDFwLEBEQ' + +'FSECEAkgAhAJDFsLEBEQDyECEAkgAhAJDFoLEBEQDxAPIQIQCRAJIAIQCQxZCxASJAEMWAsQEiMBEAgkAQxXCxASIQIQDwRAIAIkAQsMVgsQEiECEA8EQCMBEAggAiQBCwxVCxASEBcQCQxUCxASEA8Q' + +'GgxTCxAREAAQCQxSCxAREA8QASECIAIEQCACDwsMUQsQERAPahAJDFALEBEhAhAPIAJrEAkMTwsQEUEBahAJDE4LEBFBAWsQCQxNCxAREA9LEAwMTAsQERAPSRAMDEsLEBEQD0YQDAxKCxARIgIQFSAC' + +'EAlHEAwMSQsQESECEA8gAnQQCQxICxARIQIQDyACdhAJDEcLEBEhAhAPIAIQIBAJDEYLEBEhAhAPIAIQIRAJDEULEBEQD3IQCQxECxAREA9zEAkMQwsQERAPcRAJDEILEBFB/wFzEAkMQQsjDQ8LEA4Q' + +'Cgw/C0F+EB8MPgsQFBAKDD0LEBYQCgw8CxAQEBYhAhAKIAIQCgw7CxAQEBAhAhAKIAIQCgw6CxAQEBAQECECEAoQCiACEAoMOQsQECQBDDgLEBAjARAIJAEMNwsQECECEBAEQCACJAELDDYLEBAhAhAQ' + +'BEAjARAIIAIkAQsMNQsQEBAYEAoMNAsQEBAQEBsMMwsQDxAZEAoMMgsQDxAQEBwhAiACBEAgAg8LDDELEBAQEGoQCgwwCxAQIQIQECACaxAKDC8LEBBBAWoQCgwuCxAQQQFrEAoMLQsQEBAQSxAMDCwL' + +'EBAQEEkQDAwrCxAQEBBGEAwMKgsQECICEBYgAhAKRxAMDCkLEA8hAhAQIAJ0EAoMKAsQDyECEBAgAnYQCgwnCxAPIQIQECACECIQCgwmCxAPIQIQECACECMQCgwlCxAQEBByEAoMJAsQEBAQcxAKDCML' + +'EBAQEHEQCgwiCxAQQf//A3MQCgwhCyMODwsQEhAKDB8LQQIQHQweCxASIgIQCiACEAgMHQsQEiICEAogAhAKDBwLEBIQFiECEAogAhAKDBsLEBIQECECEAogAhAKDBoLEBIQEBAQIQIQChAKIAIQCgwZ' + +'CxASJAEMGAsQEiMBEAgkAQwXCxASIQIQEARAIAIkAQsMFgsQEiECEBAEQCMBEAggAiQBCwwVCxASEBgQCgwUCxASEBAQGwwTCxAREBkQCgwSCxAREBAQHCECIAIEQCACDwsMEQsQEhAQahAKDBALEBIh' + +'AhAQIAJrEAoMDwsQEkEBahAKDA4LEBJBAWsQCgwNCxASEBBLEAwMDAsQEhAQSRAMDAsLEBIQEEYQDAwKCxASIgIQFiACEApHEAwMCQsQESECEBAgAnQQCgwICxARIQIQECACdhAKDAcLEBEhAhAQIAIQ' + +'IhAKDAYLEBEhAhAQIAIQIxAKDAULEBIQEHIQCgwECxASEBBzEAoMAwsQEhAQcRAKDAILEBJB//8DcxAKDAELCyMHDws='), c => c.charCodeAt(0)); + +// WebAssembly module compiled and ready to be instantiated. +let wasmModule; + +async function loadWasmModule() { + // // Uncomment this to load the WebAssembly module bytecode from a separate file. + // wasmBytecode = await fetch('/bedrock.wasm') + // .then((r) => { return r.ok ? r.arrayBuffer() : null }) + // .then((a) => { return new Uint8Array(a) }) + // .catch(() => { return null }); + wasmModule = await WebAssembly.compile(wasmBytecode).then((m) => m); +} + + + +// ----------------------------------------------------------------------------------------------- + +// :::::: ASSEMBLER AND EMULATOR INSTANTIATION :::::::::::::::::::::::::::::::::::::::::::::::::: + +// ----------------------------------------------------------------------------------------------- + + + +// Automatically upgrade all eligible elements to assemblers or emulators. +window.addEventListener('DOMContentLoaded', ()=>{ + // Attempt to load and compile the WebAssembly module, then start upgrading. + loadWasmModule().finally(upgradeAll); +}) + + +// Upgrade every `pre.bedrock`/`bedrock` element to an assembler/emulator. +function upgradeAll() { + // Upgrade every `pre.bedrock` element to an assembler. for (let element of document.querySelectorAll('pre.bedrock')) { - // Click on the element to upgrade to an assembler. + // Defer the upgrade until the element is clicked. element.style.cursor = 'pointer'; - element.addEventListener('click', function(e) { + element.addEventListener('click', (e)=>{ e.preventDefault(); upgradeToAssembler(element); }) - // Prevent text selection on the element. - element.addEventListener('selectstart', function(e) { + // Prevent the click from selecting any text. + element.addEventListener('selectstart', (e)=>{ e.preventDefault(); }) } + // Upgrade every `bedrock` element to an emulator. for (let element of document.querySelectorAll('bedrock')) { upgradeToEmulator(element); } -}) - - -// Insert the styles for the assembler and emulator into the page. -function injectStyles() { + // Insert the CSS for the assembler and emulator into the page head. let css = document.createElement('style'); - css.textContent = bedrockStyles; + css.textContent = bedrockCSS; document.head.appendChild(css); } -// Upgrade a text element to a full assembler. +// Upgrade a text-containing element to a full assembler. function upgradeToAssembler(element) { let assembler = new AssemblerElement(element); // HACK 2: Track the top of the original element relative to the viewport. let topStart = element.getBoundingClientRect().top; let scrollStart = window.scrollY; - // HACK 1: Insert a fake spacer element following the original element to // prevent the page from jumping when the original element is replaced. - // Spacer ensures that there is at least element height below the - // viewport, filling the gap created while element is being replaced. + // Spacer ensures that there is at minimum an element-sized span below + // the viewport, filling the gap created when the element momentarily + // disappears while being replaced. let windowHeight = document.documentElement.clientHeight; let documentHeight = document.documentElement.scrollHeight; let belowViewport = documentHeight - (window.scrollY + windowHeight); @@ -58,18 +144,17 @@ function upgradeToAssembler(element) { spacer.style.height = spacerHeight + 'px'; element.parentElement.insertBefore(spacer, element.nextSibling); - element.replaceWith(assembler); + element.replaceWith(assembler.el); // HACK 1: Remove the spacer after a short delay. setTimeout(()=>spacer.remove()); - // HACK 2: Check if the page jumped as the assembler was inserted. - let topEnd = assembler.getBoundingClientRect().top; + let topEnd = assembler.el.getBoundingClientRect().top; if (topStart != topEnd) { window.scrollTo({top: scrollStart, behavior: 'instant'}); // Wait for the document layout to settle. setTimeout(() => { - topEnd = assembler.getBoundingClientRect().top; + topEnd = assembler.el.getBoundingClientRect().top; let topTarget = (topEnd - topStart) + window.scrollY; window.scrollTo({top: topTarget, behavior: 'instant'}); }); @@ -79,708 +164,47 @@ function upgradeToAssembler(element) { } -// Upgrade a bedrock element with attributes to a full emulator. +// Upgrade a bedrock element to a full emulator. Supported attributes are: +// src: URL to the Bedrock program to run +// controls: if present, show frame with controls around emulator +// nocursor: if present, hide the regular mouse cursor when hovering +// scale: initial screen scale factor (1-10) function upgradeToEmulator(element) { + // Extract attribute values. let attr = element.attributes; - if (!attr.src || !attr.src.value) { - console.error("No valid src attribute provided for <bedrock> element"); - return; - } - let scale = 1; - try { scale = clamp(parseInt(attr.scale.value), 1, 10) } catch {} - let options = { - scale, - controls: 'controls' in attr, - nocursor: 'nocursor' in attr, - autoplay: true, - }; - let url = URL.parse(attr.src.value, window.location.href); - if (!url) { - console.error(`Invalid src attribute ${attr.src.value} provided for <bedrock> element`); - return; - } - let request = new XMLHttpRequest(); - request.open("GET", url, true); - request.responseType = "arraybuffer"; - request.onreadystatechange = function() { - if (request.readyState == 4) { - if (request.status == 200) { - options.bytecode = new Uint8Array(request.response); - let emulator = new EmulatorElement(options); - element.replaceWith(emulator); - } else { - console.error(`Could not load Bedrock program from ${url.href}`); - } - } - } - request.send(); -} - - -// ----------------------------------------------------------------------------------------------- + -// :::::: ELEMENTS :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + -// ----------------------------------------------------------------------------------------------- + - - -// Constructor for an interactive assembler element. -function AssemblerElement(element) { - let assembler = elementFromHTML(assemblerTemplate); - let textarea = assembler.querySelector('textarea'); - let viewer = assembler.querySelector('.viewer'); - let editor = assembler.querySelector('.editor'); - let errorPanel = assembler.querySelector('.panel.errors'); - let bytecodePanel = assembler.querySelector('.panel.bytecode'); - let status = assembler.querySelector('.status'); - let checkButton = assembler.querySelector('button[name="check"]'); - let runButton = assembler.querySelector('button[name="run"]'); - let fullscreenButton = assembler.querySelector('button[name="fullscreen"]'); - assembler.autoGrow = true; - assembler.emulator = null; - - - // Show and hide the error and bytecode panels. - assembler.showErrorPanel = function() { - errorPanel.classList.remove('hidden'); } - assembler.hideErrorPanel = function() { - errorPanel.classList.add('hidden'); } - assembler.showBytecodePanel = function() { - bytecodePanel.classList.remove('hidden'); } - assembler.hideBytecodePanel = function() { - bytecodePanel.classList.add('hidden'); } - - - // Place focus on the editor. - assembler.focus = function() { - textarea.focus(); - } - - // Lock the viewer to the textarea by syncing their scroll positions. - assembler.syncScroll = function() { - viewer.scrollTop = textarea.scrollTop; - viewer.scrollLeft = textarea.scrollLeft; - } - - // Lock the viewer to the textarea by syncing their sizes. - assembler.syncSize = function() { - // Disable auto-grow if editor was made smaller by the user. - if (textarea.clientHeight < viewer.clientHeight) { - assembler.autoGrow = false; } - viewer.style.height = textarea.clientHeight + 'px'; - editor.style.height = textarea.clientHeight + 'px'; - } - - // Copy text from textarea to viewer with syntax highlighting. - assembler.renderText = function() { - // Force viewer to show trailing newlines. - let text = textarea.value; - if (text.slice(-1) == "\n") text += ' '; - // Replace contents of viewer. - viewer.innerHTML = ''; - for (let child of highlightSource(text)) { - viewer.appendChild(child); } - // Auto-grow the text area as text is entered. - if (assembler.autoGrow && textarea.scrollHeight > textarea.clientHeight) { - textarea.style.height = textarea.scrollHeight + 'px'; } - // Lock viewer to textarea. - assembler.syncScroll(); - } - - // Assemble the program and show bytecode or errors. - assembler.checkProgram = function() { - let { bytecode, symbols, errors } = assembleProgram(textarea.value); - errorPanel.innerHTML = ''; - bytecodePanel.innerHTML = ''; - if (errors.length) { - let list = document.createElement('ul'); - for (let error of errors) { - let item = document.createElement('li'); - let line = error.start.line + 1; - let column = error.start.column + 1; - item.textContent = `[${line}:${column}] ${error.text}`; - list.appendChild(item); } - errorPanel.appendChild(list); - let unit = errors.length == 1 ? 'error' : 'errors'; - status.textContent = `${errors.length} ${unit}`; - assembler.hideBytecodePanel(); - assembler.showErrorPanel(); - } else { - let unit = bytecode.length == 1 ? 'byte' : 'bytes'; - status.textContent = `no errors (${bytecode.length} ${unit})`; - let hexString = ''; - for (let byte of bytecode) hexString += hex(byte, 2) + ' '; - let programListing = document.createElement('div'); - programListing.textContent = hexString; - bytecodePanel.appendChild(programListing); - assembler.hideErrorPanel(); - assembler.hideBytecodePanel(); - if (bytecode.length) assembler.showBytecodePanel(); - return { bytecode, symbols }; - } - } - - assembler.runProgram = function() { - // Create an emulator if none exists. - if (!assembler.emulator) { - assembler.emulator = new EmulatorElement(); - assembler.emulator.assembler = assembler; - assembler.parentElement.insertBefore(assembler.emulator, assembler.nextSibling); } - let program = assembler.checkProgram(); - if (program) { - assembler.hideBytecodePanel(); - let { bytecode, symbols } = program; - assembler.emulator.showStatePanel(); - assembler.emulator.startProgram(bytecode, symbols); - } - } - - // Change the fullscreen state of the whole assembler. - assembler.toggleFullscreen = function() { - if (document.fullscreenElement != assembler) { - assembler.requestFullscreen(); - } else { - document.exitFullscreen(); - } - } - - // Handle keypresses on the whole assembler. - assembler.onKeyPress = function(event) { - if (event.key == "Escape") { - event.preventDefault(); - assembler.hideErrorPanel(); - assembler.hideBytecodePanel(); - } else if (event.key == "s" && event.ctrlKey) { - event.preventDefault(); - assembler.checkProgram(); - } - } - - // Handle keypresses on just the editor. - assembler.onEditorKeyPress = function(event) { - let size = 2; let indent = ' '; - let text = textarea.value; - let selectionStart = textarea.selectionStart; - let selectionEnd = textarea.selectionEnd; - - // Find the character index following the previous newline character. - function findLineStart(i) { - while (i>0 && text[i-1] != '\n') i -= 1; - return i; } - - if (event.key == "Enter" && event.ctrlKey) { - event.preventDefault(); - assembler.runProgram(); - } else if (event.key == "Tab") { - event.preventDefault(); - // Find the start index of every line in the selection. - let i = findLineStart(selectionStart); - let lineStarts = [i]; - for (; i<selectionEnd; i++) if (text[i] == '\n') lineStarts.push(i+1); - if (event.shiftKey) { - // Find length of the leading whitespace in each line. - let whitespaceLengths = []; - for (let start of lineStarts) { - let end = start; - while ('\t '.includes(text[end])) end++; - whitespaceLengths.push(end-start); } - // Unindent each line in reverse order. - selectionStart -= Math.min(size, whitespaceLengths[0]); - for (let i=lineStarts.length-1; i>=0; i--) { - let start = lineStarts[i]; - let trim = Math.min(size, whitespaceLengths[i]); - text = text.slice(0, start) + text.slice(start+trim); - selectionEnd -= trim; } - } else { - // Indent each line in reverse order. - selectionStart += size; - for (let i=lineStarts.length-1; i>=0; i--) { - let start = lineStarts[i]; - text = text.slice(0, start) + indent + text.slice(start); - selectionEnd += size; } - } - // Update textarea content and viewer. - textarea.value = text; - textarea.selectionStart = selectionStart; - textarea.selectionEnd = selectionEnd; - assembler.renderText(); - } - } - - textarea.addEventListener('input', assembler.renderText); - textarea.addEventListener('scroll', assembler.syncScroll); - textarea.addEventListener('keydown', assembler.onEditorKeyPress); - assembler.addEventListener('keydown', assembler.onKeyPress); - checkButton.addEventListener('click', assembler.checkProgram); - runButton.addEventListener('click', assembler.runProgram); - fullscreenButton.addEventListener('click', assembler.toggleFullscreen); - - assembler.resizeObserver = new ResizeObserver(assembler.syncSize).observe(textarea); - textarea.value = element.textContent.trim(); - textarea.selectionStart = 0; - textarea.selectionEnd = 0; - textarea.scrollLeft = 0; - textarea.style.height = element.clientHeight + 'px'; - assembler.renderText(); - - return assembler; -} - - -/// Constructor for a interactive emulator element. -function EmulatorElement(options) { - let emulator = elementFromHTML(emulatorTemplate); - // Menu bar - let menuBar = emulator.querySelector('.menubar'); - let fullscreenButton = menuBar.querySelector('button[name="fullscreen"]'); - let stateButton = menuBar.querySelector('button[name="state"]'); - let runButton = menuBar.querySelector('button[name="run"]'); - let pauseButton = menuBar.querySelector('button[name="pause"]'); - let stopButton = menuBar.querySelector('button[name="stop"]'); - let stepButton = menuBar.querySelector('button[name="step"]'); - let status = menuBar.querySelector('.status'); - // Screen panel - let screenPanel = emulator.querySelector('.screen'); - let canvas = screenPanel.querySelector('canvas'); - // Stream panel - let streamPanel = emulator.querySelector('.stream'); - let transmissions = emulator.querySelector('.transmissions'); - // State panel - let statePanel = emulator.querySelector('.panel.state'); - let pcState = statePanel.querySelector('.pc'); - let labelState = statePanel.querySelector('.label'); - let wstState = statePanel.querySelector('.wst'); - let rstState = statePanel.querySelector('.rst'); - - let loadedBytecode = options ? options.bytecode : new Uint8Array(0); - let loadedSymbols = new SymbolTable(); - let loadedMetadata = parseMetadata(loadedBytecode); - let currentTransmission = null; - let transmissionParser = new Utf8StreamParser(); - - emulator.screen = screenPanel; - emulator.canvas = canvas; - let br = new Bedrock(emulator); - br.reset(); - br.onUpdate = () => emulator.updateDOM(); - - emulator.showStatePanel = function() { - statePanel.classList.remove('hidden'); } - emulator.toggleStatePanel = function() { - statePanel.classList.toggle('hidden'); } - emulator.showStreamPanel = function() { - streamPanel.classList.remove('hidden'); } - emulator.hideStreamPanel = function() { - streamPanel.classList.add('hidden'); } - emulator.showScreenPanel = function() { - screenPanel.classList.remove('hidden'); } - emulator.hideScreenPanel = function() { - screenPanel.classList.add('hidden'); } - emulator.hideMenuBar = function() { - menuBar.classList.add('hidden'); } - - // Reset the emulator and load a new program. - emulator.startProgram = function(bytecode, symbols) { - emulator.stopProgram(); - loadedBytecode = bytecode; - loadedSymbols = symbols; - loadedMetadata = parseMetadata(bytecode); - br.loadProgram(bytecode); - br.run(); - } - // Fires when the play button is pressed. - emulator.runProgram = function() { - br.blank ? emulator.startProgram(loadedBytecode, loadedSymbols) : br.run(); - } - // Fires when the pause button is pressed. - emulator.pauseProgram = function() { - br.pause(); - } - // Fires when the stop button is pressed. - emulator.stopProgram = function() { - br.stop(); - emulator.updateDOM(); - transmissions.innerHTML = ''; - emulator.hideStreamPanel(); - emulator.hideScreenPanel(); - currentTransmission = null; - } - - emulator.updateDOM = function() { - function renderStack(stack) { - let string = ''; - for (let i=0; i<stack.p && i<stack.mem.length; i++) { - string += hex(stack.mem[i], 2) + ' '; - } - return string; - } - pcState.textContent = hex(br.p, 4); - labelState.textContent = loadedSymbols.findSymbol(br.p); - wstState.textContent = renderStack(br.wst); - rstState.textContent = renderStack(br.rst); - if (br.halted) { - status.textContent = 'ended'; - } else if (br.blank) { - status.textContent = ''; - } else if (br.paused) { - status.textContent = 'paused'; - } else if (br.asleep) { - let wakeMask = br.dev.system.wakeMask; - status.textContent = `sleeping / 0x${hex(wakeMask, 4)}`; - } else { - status.textContent = `running / ${br.cycle}`; - } - emulator.flushTransmission(); - } - - emulator.updateScreenSize = function() { - if (!screenPanel.classList.contains('hidden')) { - if (document.fullscreenElement == emulator) { - let width = screenPanel.clientWidth; - let height = screenPanel.clientHeight; - br.dev.screen.resizeToFill(width, height); - } else { - let width = screenPanel.clientWidth; - let height = width * 9/16; - br.dev.screen.resizeToFill(width, height); - } - } - } - - // Receive an incoming byte from the local stream. - emulator.receiveTransmissionByte = function(byte) { - transmissionParser.push(byte); - } - - // Push all received bytes to the DOM. - emulator.flushTransmission = function() { - let string = transmissionParser.read(); - if (string) { - if (!currentTransmission) { - emulator.showStreamPanel(); - let element = document.createElement('li'); - element.addEventListener('click', function() { - copyText(element.textContent); }) - currentTransmission = element; - transmissions.appendChild(element); } - currentTransmission.textContent += string; - streamPanel.scrollTop = streamPanel.scrollHeight; - } - } - - // End the current incoming transmission. - emulator.endTransmission = function() { - emulator.flushTransmission(); - streamPanel.scrollTop = streamPanel.scrollHeight; - currentTransmission = null; - - } - - // Change the fullscreen state of the whole emulator. - emulator.toggleFullscreen = function() { - if (document.fullscreenElement != emulator) { - emulator.requestFullscreen(); - } else { - document.exitFullscreen(); - } - } - - emulator.mouseMove = function(e) { - let bounds = canvas.getBoundingClientRect(); - let scale = br.dev.screen.scale; - let x = ((e.clientX - bounds.left) / scale) & 0xFFFF; - let y = ((e.clientY - bounds.top ) / scale) & 0xFFFF; - br.dev.input.applyPosition(x, y); - } - emulator.mouseDown = function(e) { - br.dev.input.applyButtons(e.buttons); - } - emulator.mouseUp = function(e) { - br.dev.input.applyButtons(e.buttons); - } - emulator.mouseEnter = function(e) { - br.dev.input.applyActive(true); - emulator.mouseMove(e); - } - emulator.mouseExit = function(e) { - br.dev.input.applyActive(false); - emulator.mouseMove(e); - } - emulator.mouseScroll = function(e) { - // Only capture scroll events if canvas is focused. - if (document.activeElement == canvas) { - e.preventDefault(); - let scale; - // TODO: Dial these numbers in a bit. - switch (e.deltaMode) { - case (e.DOM_DELTA_PIXEL): scale = 1/10; break; - case (e.DOM_DELTA_LINE): scale = 1; break; - case (e.DOM_DELTA_PAGE): scale = 20; break; - }; - br.dev.input.applyHScroll(e.deltaX * scale); - br.dev.input.applyVScroll(e.deltaY * scale); - } - } - emulator.keyInput = function(e, pressed) { - e.preventDefault(); - br.dev.input.applyModifiers(e); - if (!e.repeat && !e.isComposing) { - br.dev.input.applyKey(e, pressed); - } - } - - emulator.touchStart = function(e) { - if (e.changedTouches.length) { - emulator.mouseMove(e.changedTouches[0]); } - br.dev.input.applyActive(true); - br.dev.input.applyButtons(0x01); - } - emulator.touchEnd = function(e) { - br.dev.input.applyActive(false); - br.dev.input.applyButtons(0x00); - } - - fullscreenButton.addEventListener('click', emulator.toggleFullscreen); - stateButton.addEventListener('click', emulator.toggleStatePanel); - runButton.addEventListener('click', emulator.runProgram); - pauseButton.addEventListener('click', emulator.pauseProgram); - stopButton.addEventListener('click', emulator.stopProgram); - stepButton.addEventListener('click', br.step); - - canvas.addEventListener('mousemove', emulator.mouseMove); - canvas.addEventListener('pointermove', emulator.mouseMove); - canvas.addEventListener('mousedown', emulator.mouseDown); - canvas.addEventListener('mouseup', emulator.mouseUp); - canvas.addEventListener('touchstart', emulator.touchStart); - canvas.addEventListener('touchend', emulator.touchEnd); - canvas.addEventListener('touchcancel', emulator.touchEnd); - canvas.addEventListener('mouseenter', emulator.mouseEnter); - canvas.addEventListener('mouseleave', emulator.mouseExit); - canvas.addEventListener('wheel', emulator.mouseScroll); - canvas.addEventListener('contextmenu', (e)=>{e.preventDefault()}); - canvas.addEventListener('keydown', (e)=>emulator.keyInput(e, true)); - canvas.addEventListener('keyup', (e)=>emulator.keyInput(e, false)); - // Make canvas focusable to be able to receive keydown and keyup events. - canvas.tabIndex = 0; - - emulator.resizeObserver = new ResizeObserver(emulator.updateScreenSize).observe(screenPanel); - - if (options && !options.controls) emulator.hideMenuBar(); - if (options && options.autoplay) emulator.runProgram(); - if (options && options.nocursor) canvas.style.cursor = 'none'; - - return emulator; -} - - -// Create an element from an HTML string. -function elementFromHTML(html) { - let template = document.createElement('template'); - template.innerHTML = html.trim(); - return template.content.firstChild; -} - - -// Convert an integer to a hexadecimal string. -function hex(value, pad) { - return value.toString(16).toUpperCase().padStart(pad, '0'); -} - - -function parseMetadata(bytecode) { - // Test identifier to see if program has metadata. - let id = [0xE8,0x00,0x18,0x42,0x45,0x44,0x52,0x4F,0x43,0x4B]; - for (let i=0; i<10; i++) if (bytecode[i] != id[i]) return; - // Parse each metadata item. - let name = getString(0x000A); - let authors = getString(0x000C); - let description = getString(0x000E); - let bgColour = getColour(0x0010); - let fgColour = getColour(0x0012); - let smallIcon = getIcon(0x0014, 24); - let largeIcon = getIcon(0x0016, 64); - return { name, authors, description, bgColour, fgColour, smallIcon, largeIcon }; - - function getByte(addr) { - return bytecode[addr] || 0; - } - function getDouble(addr) { - return getByte(addr) << 8 | getByte(addr+1); - } - function getString(addr) { - addr = getDouble(addr); - if (addr) { - let parser = new Utf8StreamParser(); - while (true) { - let byte = bytecode[addr++]; - if (!byte) break; - parser.push(byte); - } - return parser.read(); - } - } - function getColour(addr) { - addr = getDouble(addr); - if (addr) { - let colour = getDouble(addr); - let r = (colour >> 8 & 0xF) * 17; - let g = (colour >> 8 & 0xF) * 17; - let b = (colour >> 8 & 0xF) * 17; - return [r, g, b]; - } - } - function getIcon(addr, size) { - // TODO: Return a custom type containing the dimensions and raw - // pixel data, and a colourise method to generate a coloured ImageData. - } -} - - -// Copy a string to the clipboard. -function copyText(text) { - navigator.clipboard.writeText(text); -} - -let encoder = new TextEncoder(); -function encodeUtf8(text) { - return encoder.encode(text); -} - -// Incrementally parse a UTF-8 string. -function Utf8StreamParser() { - // Full characters parsed so far. - this.string = ''; - // Current partial code point. - this.codePoint; - // True if the current code point is being dropped. - this.dropBytes = false; - // Number of bytes received of the current code point. - this.i = 0; - // Expected byte length of the current code point. - this.len = 0; - - // Push a code point to the string. - let pushCodePoint = (codePoint) => { - this.string += String.fromCodePoint(codePoint); - } - // Drop the current partial code point, if any. - let dropCodePoint = () => { - // Push a 'replacement character' if there is a partial code point - // that hasn't already been dropped. - if (this.i && !this.dropBytes) pushCodePoint(0xFFFD); - this.codePoint = 0; - this.dropBytes = false; - this.i = 0; - this.len = 0; - } - // Parse the start of an encoded code point. - let parseStartByte = (byte, length) => { - dropCodePoint(); - this.len = length; - parseContinuationByte(byte); - } - // Parse the remaining bytes of an encoded code point. - let parseContinuationByte = (byte) => { - if (!this.dropBytes) { - if (this.len) { - this.codePoint = this.codePoint << 6 | (byte & 0x3F); - this.i += 1; - if (this.i == this.len) { - pushCodePoint(this.codePoint); - this.len = 0; - } - } else { - this.dropBytes = true; - pushCodePoint(0xFFFD); - } - } - } + let getString = (key) => attr[key] ? attr[key].value : null; + let getBool = (key) => attr[key] ? true : false; + let src = getString("src"); + let scale = getString("scale") || "1"; + let controls = getBool("controls"); + let nocursor = getBool("nocursor"); - // Parse a UTF-8 encoded byte. - this.push = (byte) => { - if (byte < 0x80) { // start of a 1-byte code point - dropCodePoint(); - pushCodePoint(byte); - } else if (byte < 0xC0) { // not a start byte - parseContinuationByte(byte); - } else if (byte < 0xE0) { // start of a 2-byte code point - parseStartByte(byte, 2); - } else if (byte < 0xF0) { // start of a 3-byte code point - parseStartByte(byte, 3); - } else if (byte < 0xF8) { // start of a 4-byte code point - parseStartByte(byte, 4); + // Validate options. + if (!src) { console.error("No src attribute provided for emulator", element); return; } + try { scale = clamp(parseInt(attr.scale.value), 1, 10) } catch { scale = 1 } + let options = { scale, controls, nocursor, autoplay: true }; + let url = new URL(src, window.location.href).href; + // Instantiate emulator once program has loaded in. + fetch(url).then((r) => { + if (r.ok) { r.arrayBuffer().then((a) => { + options.bytecode = new Uint8Array(a); + let emulator = new EmulatorElement(options); + emulator.init().then(() => { element.replaceWith(emulator.el) }); + })} else { + console.log(`Could not load Bedrock program from ${url}`); } - } - // Parse an array of bytes. - this.pushList = (list) => { - for (let byte of list) this.push(byte); - } - - // Take the characters that have been parsed so far. - this.read = () => { - let output = this.string; - this.string = ''; - return output; - } + }); } - - - // ----------------------------------------------------------------------------------------------- + // :::::: ASSEMBLER:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + // ----------------------------------------------------------------------------------------------- + -// Convert a string of Bedrock source code into syntax-highlighted nodes. -function highlightSource(source) { - let currentText = '', currentClass = ''; - let items = []; - let typeLookup = { - 'comment': 'comment', - 'markOpen': 'comment', - 'markClose': 'comment', - 'blockOpen': '', - 'blockClose': '', - 'rawString': 'value', - 'terminatedString': 'value', - 'macroDefinition': 'definition', - 'globalLabel': 'definition', - 'localLabel': 'definition', - 'macroTerminator': 'definition', - 'spacer': '', - 'symbol': 'reference', - 'byteLiteral': 'value', - 'doubleLiteral': 'value', - 'instruction': '', - 'whitespace': '', - }; - function pushItem(text, className) { - if (currentClass != className) { - if (currentClass) { - let span = document.createElement('span'); - span.textContent = currentText; - span.className = currentClass; - items.push(span); - } else { - items.push(document.createTextNode(currentText)); - } - currentClass = className; - currentText = text; - } else { - currentText += text; - } - } - for (let token of tokeniseProgram(source).tokens) { - pushItem(token.text, typeLookup[token.type]); - } - pushItem(' '); - return items; -} - - // Tokenise a string of Bedrock source code. -function tokeniseProgram(source) { +function tokeniseSource(source) { let tokens = [], errors = []; let currentText = '', endChar = '', label = ''; let currentLine = 0, currentColumn = 0; // position of next character @@ -845,10 +269,10 @@ function tokeniseProgram(source) { } else if (firstChar == ';') { token.type = 'macroTerminator'; } else if (firstChar == '#') { - token.type = 'spacer'; + token.type = 'padding'; token.value = parseLiteral(currentText.slice(1)); if (token.value == undefined) { - error(`Invalid value for spacer: ${currentText.slice(1)}`) + error(`Invalid value for padding: ${currentText.slice(1)}`) } } else if (firstChar == '~') { token.type = 'symbol'; @@ -926,9 +350,56 @@ function tokeniseProgram(source) { } +// Convert a string of Bedrock source code into syntax-highlighted nodes. +function highlightSource(source) { + let currentText = '', currentClass = ''; + let items = []; + let typeLookup = { + 'comment': 'comment', + 'markOpen': 'comment', + 'markClose': 'comment', + 'blockOpen': '', + 'blockClose': '', + 'rawString': 'value', + 'terminatedString': 'value', + 'macroDefinition': 'definition', + 'globalLabel': 'definition', + 'localLabel': 'definition', + 'macroTerminator': 'definition', + 'padding': '', + 'symbol': 'reference', + 'byteLiteral': 'value', + 'doubleLiteral': 'value', + 'instruction': '', + 'whitespace': '', + }; + function pushItem(text, className) { + if (currentClass != className) { + if (currentClass) { + let span = document.createElement('span'); + span.textContent = currentText; + span.className = currentClass; + items.push(span); + } else { + items.push(document.createTextNode(currentText)); + } + currentClass = className; + currentText = text; + } else { + currentText += text; + } + } + for (let token of tokeniseSource(source).tokens) { + pushItem(token.text, typeLookup[token.type]); + } + pushItem(' '); + return items; +} + + // Assemble a string of Bedrock source code. -function assembleProgram(source) { - let { tokens, errors } = tokeniseProgram(source); +function assembleSource(source) { + let { tokens, errors } = tokeniseSource(source); let program = new Uint8Array(65536); // Populated up front with all names. let macros = {}, labels = {}, address = 0; @@ -971,7 +442,7 @@ function assembleProgram(source) { } else if (['rawString', 'terminatedString'].includes(token.type)) { for (let byte of encodeUtf8(token.value)) program[address++] = byte; if (token.type == 'terminatedString') pushByte(0); - } else if (token.type == 'spacer') { + } else if (token.type == 'padding') { for (let i=0; i<token.value; i++) pushByte(0); } else if (token.type == 'blockOpen') { stack.push({ token, address }); @@ -1082,6 +553,7 @@ function assembleProgram(source) { } +// Efficient lookup table for finding the closest label to a given address. function SymbolTable() { this.names = []; this.addresses = []; @@ -1117,177 +589,144 @@ function SymbolTable() { } +// Map the predefined instruction names to bytes for the assembler. +const instructionTable = { + 'HLT': 0x00, 'NOP' : 0x20, 'DB1' : 0x40, 'DB2' : 0x60, 'DB3' : 0x80, 'DB4' : 0xA0, 'DB5' : 0xC0, 'DB6' : 0xE0, + 'PSH': 0x01, 'PSH:': 0x21, 'PSH*': 0x41, 'PSH*:': 0x61, 'PSHr': 0x81, 'PSHr:': 0xA1, 'PSHr*': 0xC1, 'PSHr*:': 0xE1, + ':': 0x21, '*:': 0x61, 'r:': 0xA1, 'r*:': 0xE1, + 'POP': 0x02, 'POP:': 0x22, 'POP*': 0x42, 'POP*:': 0x62, 'POPr': 0x82, 'POPr:': 0xA2, 'POPr*': 0xC2, 'POPr*:': 0xE2, + 'CPY': 0x03, 'CPY:': 0x23, 'CPY*': 0x43, 'CPY*:': 0x63, 'CPYr': 0x83, 'CPYr:': 0xA3, 'CPYr*': 0xC3, 'CPYr*:': 0xE3, + 'DUP': 0x04, 'DUP:': 0x24, 'DUP*': 0x44, 'DUP*:': 0x64, 'DUPr': 0x84, 'DUPr:': 0xA4, 'DUPr*': 0xC4, 'DUPr*:': 0xE4, + 'OVR': 0x05, 'OVR:': 0x25, 'OVR*': 0x45, 'OVR*:': 0x65, 'OVRr': 0x85, 'OVRr:': 0xA5, 'OVRr*': 0xC5, 'OVRr*:': 0xE5, + 'SWP': 0x06, 'SWP:': 0x26, 'SWP*': 0x46, 'SWP*:': 0x66, 'SWPr': 0x86, 'SWPr:': 0xA6, 'SWPr*': 0xC6, 'SWPr*:': 0xE6, + 'ROT': 0x07, 'ROT:': 0x27, 'ROT*': 0x47, 'ROT*:': 0x67, 'ROTr': 0x87, 'ROTr:': 0xA7, 'ROTr*': 0xC7, 'ROTr*:': 0xE7, + 'JMP': 0x08, 'JMP:': 0x28, 'JMP*': 0x48, 'JMP*:': 0x68, 'JMPr': 0x88, 'JMPr:': 0xA8, 'JMPr*': 0xC8, 'JMPr*:': 0xE8, + 'JMS': 0x09, 'JMS:': 0x29, 'JMS*': 0x49, 'JMS*:': 0x69, 'JMSr': 0x89, 'JMSr:': 0xA9, 'JMSr*': 0xC9, 'JMSr*:': 0xE9, + 'JCN': 0x0A, 'JCN:': 0x2A, 'JCN*': 0x4A, 'JCN*:': 0x6A, 'JCNr': 0x8A, 'JCNr:': 0xAA, 'JCNr*': 0xCA, 'JCNr*:': 0xEA, + 'JCS': 0x0B, 'JCS:': 0x2B, 'JCS*': 0x4B, 'JCS*:': 0x6B, 'JCSr': 0x8B, 'JCSr:': 0xAB, 'JCSr*': 0xCB, 'JCSr*:': 0xEB, + 'LDA': 0x0C, 'LDA:': 0x2C, 'LDA*': 0x4C, 'LDA*:': 0x6C, 'LDAr': 0x8C, 'LDAr:': 0xAC, 'LDAr*': 0xCC, 'LDAr*:': 0xEC, + 'STA': 0x0D, 'STA:': 0x2D, 'STA*': 0x4D, 'STA*:': 0x6D, 'STAr': 0x8D, 'STAr:': 0xAD, 'STAr*': 0xCD, 'STAr*:': 0xED, + 'LDD': 0x0E, 'LDD:': 0x2E, 'LDD*': 0x4E, 'LDD*:': 0x6E, 'LDDr': 0x8E, 'LDDr:': 0xAE, 'LDDr*': 0xCE, 'LDDr*:': 0xEE, + 'STD': 0x0F, 'STD:': 0x2F, 'STD*': 0x4F, 'STD*:': 0x6F, 'STDr': 0x8F, 'STDr:': 0xAF, 'STDr*': 0xCF, 'STDr*:': 0xEF, + 'ADD': 0x10, 'ADD:': 0x30, 'ADD*': 0x50, 'ADD*:': 0x70, 'ADDr': 0x90, 'ADDr:': 0xB0, 'ADDr*': 0xD0, 'ADDr*:': 0xF0, + 'SUB': 0x11, 'SUB:': 0x31, 'SUB*': 0x51, 'SUB*:': 0x71, 'SUBr': 0x91, 'SUBr:': 0xB1, 'SUBr*': 0xD1, 'SUBr*:': 0xF1, + 'INC': 0x12, 'INC:': 0x32, 'INC*': 0x52, 'INC*:': 0x72, 'INCr': 0x92, 'INCr:': 0xB2, 'INCr*': 0xD2, 'INCr*:': 0xF2, + 'DEC': 0x13, 'DEC:': 0x33, 'DEC*': 0x53, 'DEC*:': 0x73, 'DECr': 0x93, 'DECr:': 0xB3, 'DECr*': 0xD3, 'DECr*:': 0xF3, + 'LTH': 0x14, 'LTH:': 0x34, 'LTH*': 0x54, 'LTH*:': 0x74, 'LTHr': 0x94, 'LTHr:': 0xB4, 'LTHr*': 0xD4, 'LTHr*:': 0xF4, + 'GTH': 0x15, 'GTH:': 0x35, 'GTH*': 0x55, 'GTH*:': 0x75, 'GTHr': 0x95, 'GTHr:': 0xB5, 'GTHr*': 0xD5, 'GTHr*:': 0xF5, + 'EQU': 0x16, 'EQU:': 0x36, 'EQU*': 0x56, 'EQU*:': 0x76, 'EQUr': 0x96, 'EQUr:': 0xB6, 'EQUr*': 0xD6, 'EQUr*:': 0xF6, + 'NQK': 0x17, 'NQK:': 0x37, 'NQK*': 0x57, 'NQK*:': 0x77, 'NQKr': 0x97, 'NQKr:': 0xB7, 'NQKr*': 0xD7, 'NQKr*:': 0xF7, + 'SHL': 0x18, 'SHL:': 0x38, 'SHL*': 0x58, 'SHL*:': 0x78, 'SHLr': 0x98, 'SHLr:': 0xB8, 'SHLr*': 0xD8, 'SHLr*:': 0xF8, + 'SHR': 0x19, 'SHR:': 0x39, 'SHR*': 0x59, 'SHR*:': 0x79, 'SHRr': 0x99, 'SHRr:': 0xB9, 'SHRr*': 0xD9, 'SHRr*:': 0xF9, + 'ROL': 0x1A, 'ROL:': 0x3A, 'ROL*': 0x5A, 'ROL*:': 0x7A, 'ROLr': 0x9A, 'ROLr:': 0xBA, 'ROLr*': 0xDA, 'ROLr*:': 0xFA, + 'ROR': 0x1B, 'ROR:': 0x3B, 'ROR*': 0x5B, 'ROR*:': 0x7B, 'RORr': 0x9B, 'RORr:': 0xBB, 'RORr*': 0xDB, 'RORr*:': 0xFB, + 'IOR': 0x1C, 'IOR:': 0x3C, 'IOR*': 0x5C, 'IOR*:': 0x7C, 'IORr': 0x9C, 'IORr:': 0xBC, 'IORr*': 0xDC, 'IORr*:': 0xFC, + 'XOR': 0x1D, 'XOR:': 0x3D, 'XOR*': 0x5D, 'XOR*:': 0x7D, 'XORr': 0x9D, 'XORr:': 0xBD, 'XORr*': 0xDD, 'XORr*:': 0xFD, + 'AND': 0x1E, 'AND:': 0x3E, 'AND*': 0x5E, 'AND*:': 0x7E, 'ANDr': 0x9E, 'ANDr:': 0xBE, 'ANDr*': 0xDE, 'ANDr*:': 0xFE, + 'NOT': 0x1F, 'NOT:': 0x3F, 'NOT*': 0x5F, 'NOT*:': 0x7F, 'NOTr': 0x9F, 'NOTr:': 0xBF, 'NOTr*': 0xDF, 'NOTr*:': 0xFF, +}; + + // ----------------------------------------------------------------------------------------------- + -// :::::: EMULATOR::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + +// :::::: EMULATOR CORE (WEBASSEMBLY) ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + // ----------------------------------------------------------------------------------------------- + -// The core Bedrock emulator. Receives an EmulatorElement. -function Bedrock(e) { - this.e = e; - this.mem = new Uint8Array(65536); - this.wst = new Stack(); - this.rst = new Stack(); - this.dev = new DeviceBus(this); - this.mark = performance.now(); - // Callback functions. - this.onUpdate; +// WebAssembly implementation of the Bedrock core. +function BedrockWasm() { + let wasm, wst, rst; + this.mem; + this.ldd; + this.std; - // Reset the emulator, preserving the contents of program memory. - this.reset = () => { - this.p = 0; - this.wst.reset(); - this.rst.reset(); - this.dev.reset(); - this.cycle = 0; - this.blank = true; // true when emulator has been reset. - this.paused = true; // true when program is paused for the user. - this.asleep = false; // true when program is waiting for input. - this.halted = false; // true when program has halted. - this.frameLag = 0; // time since runLoop started + this.init = async (dget1, dset1) => { + const imported = { bedrock: { dget1, dset1 } }; + wasm = (await WebAssembly.instantiate(wasmModule, imported)).exports; + this.mem = new Uint8Array(wasm.memory.buffer, 0, 0x10000); + wst = new Uint8Array(wasm.memory.buffer, 0x10000, 0x100); + rst = new Uint8Array(wasm.memory.buffer, 0x10100, 0x100); + this.ldd = dget1; + this.std = dset1; + this.cc = wasm.cc; + this.ip = wasm.ip; + this.wp = wasm.wp; + this.rp = wasm.rp; + this.wst = (i) => { return wst[0xFF-i]; } + this.rst = (i) => { return rst[0xFF-i]; } + this.eval = wasm.eval; + this.reset = wasm.reset; } - // Reset the emulator and load in a new program. - this.loadProgram = (bytecode) => { + // Load an assembled program into program memory. + this.load = (bytes) => { this.reset(); this.mem.fill(0); - this.mem.set(bytecode.slice(0, 65536)); - this.blank = false; - this.mark = performance.now(); - console.log("Started new run"); + this.mem.set(bytes.slice(0, 65536)); } +} - this.run = () => { - this.paused = false; - this.update(); - this.runLoop(); - } - this.step = () => { - if (this.paused && !this.halted) { - if (this.asleep) { - this.sleepLoop() - } else { - this.blank = false; - switch (this.evaluate(1)) { - case 5: this.halt(); break; // HALT - case 4: this.reset(); break; // RESET - case 3: this.reset(); break; // FORK - case 2: this.sleep(); break; // SLEEP - case 1: this.pause(); break; // DEBUG - default: break; // NORMAL - } - this.update(); - this.render(); - } - } - } - this.pause = () => { - this.paused = true; - this.update(); - this.render(); - } - - this.stop = () => { - this.reset(); - this.update(); - this.render(); - } +// ----------------------------------------------------------------------------------------------- + +// :::::: EMULATOR CORE (JAVASCRIPT) :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + +// ----------------------------------------------------------------------------------------------- + - this.halt = () => { - this.halted = true; - this.update(); - this.render(); - console.log("Run completed in", performance.now() - this.mark, "milliseconds"); - } - this.sleep = () => { - this.asleep = true; - this.update(); - this.render(); - this.sleepLoop(); - } +// Pure JavaScript implementation of the Bedrock core. +function BedrockJs() { + this.mem; + this.ldd; + this.std; + let cc, ip, wst, rst; - this.update = () => { - if (this.onUpdate) this.onUpdate(this); + this.init = async (dget1, dset1) => { + this.mem = new Uint8Array(65536); + this.ldd = dget1; + this.std = dset1; + cc = 0; + ip = 0; + wst = new Stack(); + rst = new Stack(); } - // Render the screen contents to the canvas. - this.render = () => { - if (this.dev.input.accessed || this.dev.screen.accessed) { - e.showScreenPanel(); - } - let scr = this.dev.screen; - if (scr.dirty()) { - scr.render(); - let image = new ImageData(scr.pixels, scr.width, scr.height); - scr.ctx.putImageData(image, 0, 0, scr.dx0, scr.dy0, scr.dx1, scr.dy1); - scr.unmark(); - this.frameLag = performance.now(); - } - } + this.cc = () => { return cc }; + this.ip = () => { return ip }; + this.wp = () => { return wst.p }; + this.rp = () => { return rst.p }; + this.wst = (i) => { return wst.mem[i] }; + this.rst = (i) => { return rst.mem[i] }; - this.runLoop = () => { - if (!this.halted && !this.paused) { - if (this.asleep) { - setTimeout(this.sleepLoop.bind(this)); - } else { - // Tail-recursive with timeout to allow other code to run. - switch (this.evaluate(100000)) { - case 5: this.halt(); return; // HALT - case 4: this.reset(); break; // RESET - case 3: this.reset(); break; // FORK - case 2: this.sleep(); return; // SLEEP - case 1: this.pause(); break; // DEBUG - default: // NORMAL - } - this.update(); - // Force a render if the frame has been running for >500ms. - if (performance.now() - this.frameLag > 500) { - this.render(); } - setTimeout(this.runLoop.bind(this), 0); - } - } + // Load an assembled program into program memory. + this.load = (bytes) => { + this.reset(); + this.mem.fill(0); + this.mem.set(bytes.slice(0, 65536)); } - this.sleepLoop = () => { - this.frameLag = performance.now(); - if (!this.halted) { - if (!this.asleep) { - setTimeout(this.runLoop.bind(this), 0); - } else { - let queue = this.dev.queue.get(this.dev.system.wakeMask); - for (let slot of queue) { - if (this.dev.devices[slot].wake()) { - this.asleep = false; - this.dev.system.wakeSlot = slot; - setTimeout(this.runLoop.bind(this)); - return; - } - } - setTimeout(this.sleepLoop.bind(this), 0); - } - } + // Reset all pointers and counters to zero. + this.reset = () => { + cc = 0; + ip = 0; + wst.reset(); + rst.reset(); } - // Evaluate at most n processor cycles, returning a signal value. - this.evaluate = (n) => { - for (let i=0; i<n; i++) { - this.cycle += 1; - let instr = this.mem[this.p++]; + this.eval = (maxCycles) => { + for (let i=0; i<maxCycles; i++) { + cc += 1; + let instr = this.mem[ip++]; let rMode = instr & 0x80; let wMode = instr & 0x40; let iMode = instr & 0x20; - let w = rMode ? this.rst : this.wst; - let r = rMode ? this.wst : this.rst; + let w = rMode ? rst : wst; + let r = rMode ? wst : rst; // Memory access functions. - let mpop1 = ( ) => { return this.mem[this.p++]; } + let mpop1 = ( ) => { return this.mem[ip++]; } let mpop2 = ( ) => { return mpop1() << 8 | mpop1(); } let mpopx = ( ) => { return wMode ? mpop2() : mpop1(); } - let mpsh1 = (v) => { this.mem[this.p++] = v; } + let mpsh1 = (v) => { this.mem[ip++] = v; } let mpsh2 = (v) => { this.mpsh1(v >> 8); this.mpsh1(v); } let mpshx = (v) => { wMode ? mpsh2(v) : mpsh1(v); } let mget1 = (a) => { return this.mem[a]; } @@ -1297,18 +736,18 @@ function Bedrock(e) { let mset2 = (a,v) => { mset1(a, v >> 8); mset1(a+1, v); } let msetx = (a,v) => { wMode ? mset2(a,v) : mset1(a,v); } // Device access functions. - let dget1 = (p) => { return this.dev.read(p); } + let dget1 = this.ldd; let dget2 = (p) => { return dget1(p) << 8 | dget1(p+1); } let dgetx = (p) => { return wMode ? dget2(p) : dget1(p); } - let dset1 = (p,v) => { return this.dev.write(p,v); } + let dset1 = this.std; let dset2 = (p,v) => { let s1 = dset1(p, v >> 8); let s2 = dset1(p+1, v & 0xFF); return s1 || s2; } let dsetx = (p,v) => { if (wMode) { return dset2(p,v) } else { return dset1(p,v) } } // Stack access functions. - let wpop1 = ( ) => { return iMode ? (iMode=0, mpop1()) : w.pop1(); } - let wpop2 = ( ) => { return iMode ? (iMode=0, mpop2()) : w.pop2(); } + let wpop1 = ( ) => { return w.pop1(); } + let wpop2 = ( ) => { return w.pop2(); } let wpopx = ( ) => { return wMode ? wpop2() : wpop1(); } - let rpop1 = ( ) => { return iMode ? (iMode=0, mpop1()) : r.pop1(); } - let rpop2 = ( ) => { return iMode ? (iMode=0, mpop2()) : r.pop2(); } + let rpop1 = ( ) => { return r.pop1(); } + let rpop2 = ( ) => { return r.pop2(); } let rpopx = ( ) => { return wMode ? rpop2() : rpop1(); } let wpsh1 = (v) => { w.psh1(v); } let wpsh2 = (v) => { w.psh2(v); } @@ -1316,6 +755,13 @@ function Bedrock(e) { let rpsh1 = (v) => { r.psh1(v); } let rpsh2 = (v) => { r.psh2(v); } let rpshx = (v) => { wMode ? r.psh2(v) : r.psh1(v); } + // Immediate-mode aware stack access functions. + let iwpop1 = ( ) => { return iMode ? mpop1() : w.pop1(); } + let iwpop2 = ( ) => { return iMode ? mpop2() : w.pop2(); } + let iwpopx = ( ) => { return wMode ? iwpop2() : iwpop1(); } + let irpop1 = ( ) => { return iMode ? mpop1() : r.pop1(); } + let irpop2 = ( ) => { return iMode ? mpop2() : r.pop2(); } + let irpopx = ( ) => { return wMode ? irpop2() : irpop1(); } // Shifting operations. let shl1 = (v,d) => { return d < 8 ? v << d & 0xFF : 0; } let shl2 = (v,d) => { return d < 16 ? v << d & 0xFFFF : 0; } @@ -1332,45 +778,55 @@ function Bedrock(e) { let a, p, s, t, v, x, y, z; switch (instr & 0x1F) { - /* HLT */ case 0x00: switch(instr) { case 0x00: return 5; case 0x40: return 1; default: } break; - /* PSH */ case 0x01: x=rpopx(); wpshx(x); break; - /* POP */ case 0x02: wpopx(); break; - /* CPY */ case 0x03: x=rpopx(); rpshx(x); wpshx(x); break; - /* DUP */ case 0x04: x=wpopx(); wpshx(x); wpshx(x); break; - /* OVR */ case 0x05: y=wpopx(); x=wpopx(); wpshx(x); wpshx(y); wpshx(x); break; - /* SWP */ case 0x06: y=wpopx(); x=wpopx(); wpshx(y); wpshx(x); break; - /* ROT */ case 0x07: z=wpopx(); y=wpopx(); x=wpopx(); wpshx(y); wpshx(z); wpshx(x); break; - /* JMP */ case 0x08: a=wpop2(); this.p = a; break; - /* JMS */ case 0x09: a=wpop2(); rpsh2(this.p); this.p = a; break; - /* JCN */ case 0x0A: a=wpop2(); t=wpopx(); if (t) { this.p = a; } break; - /* JCS */ case 0x0B: a=wpop2(); t=wpopx(); if (t) { rpsh2(this.p); this.p = a; } break; - /* LDA */ case 0x0C: a=wpop2(); v=mgetx(a); wpshx(v); break; - /* STA */ case 0x0D: a=wpop2(); v=wpopx(); msetx(a,v); break; - /* LDD */ case 0x0E: p=wpop1(); v=dgetx(p); wpshx(v); break; - /* STD */ case 0x0F: p=wpop1(); v=wpopx(); s=dsetx(p,v); if (s) { return s; } break; - /* ADD */ case 0x10: y=wpopx(); x=wpopx(); wpshx(x+y); break; - /* SUB */ case 0x11: y=wpopx(); x=wpopx(); wpshx(x-y); break; - /* INC */ case 0x12: x=wpopx(); wpshx(x+1); break; - /* DEC */ case 0x13: x=wpopx(); wpshx(x-1); break; - /* LTH */ case 0x14: y=wpopx(); x=wpopx(); wpsh1(0-(x <y)); break; - /* GTH */ case 0x15: y=wpopx(); x=wpopx(); wpsh1(0-(x >y)); break; - /* EQU */ case 0x16: y=wpopx(); x=wpopx(); wpsh1(0-(x==y)); break; - /* NQK */ case 0x17: y=wpopx(); x=wpopx(); wpshx(x); wpshx(y); wpsh1(0-(x!=y)); break; - /* SHL */ case 0x18: y=wpop1(); x=wpopx(); wpshx(shlx(x,y)); break; - /* SHR */ case 0x19: y=wpop1(); x=wpopx(); wpshx(shrx(x,y)); break; - /* ROL */ case 0x1A: y=wpop1(); x=wpopx(); wpshx(rolx(x,y)); break; - /* ROR */ case 0x1B: y=wpop1(); x=wpopx(); wpshx(rorx(x,y)); break; - /* IOR */ case 0x1C: y=wpopx(); x=wpopx(); wpshx(x|y); break; - /* XOR */ case 0x1D: y=wpopx(); x=wpopx(); wpshx(x^y); break; - /* AND */ case 0x1E: y=wpopx(); x=wpopx(); wpshx(x&y); break; - /* NOT */ case 0x1F: x=wpopx(); wpshx(~x ); break; + /* HLT */ case 0x00: switch(instr) { + case 0x00: return Signal.HALT; + case 0x20: continue; + case 0x40: return Signal.DB1; + case 0x60: return Signal.DB2; + case 0x80: return Signal.DB3; + case 0xA0: return Signal.DB4; + case 0xC0: return Signal.DB5; + case 0xE0: return Signal.DB6; + } + /* PSH */ case 0x01: x=irpopx(); wpshx(x); break; + /* POP */ case 0x02: iwpopx(); break; + /* CPY */ case 0x03: x=irpopx(); rpshx(x); wpshx(x); break; + /* DUP */ case 0x04: x=iwpopx(); wpshx(x); wpshx(x); break; + /* OVR */ case 0x05: y=iwpopx(); x=wpopx(); wpshx(x); wpshx(y); wpshx(x); break; + /* SWP */ case 0x06: y=iwpopx(); x=wpopx(); wpshx(y); wpshx(x); break; + /* ROT */ case 0x07: z=iwpopx(); y=wpopx(); x=wpopx(); wpshx(y); wpshx(z); wpshx(x); break; + /* JMP */ case 0x08: a=iwpop2(); ip = a; break; + /* JMS */ case 0x09: a=iwpop2(); rpsh2(ip); ip = a; break; + /* JCN */ case 0x0A: a=iwpop2(); t=wpopx(); if (t) { ip = a; } break; + /* JCS */ case 0x0B: a=iwpop2(); t=wpopx(); if (t) { rpsh2(ip); ip = a; } break; + /* LDA */ case 0x0C: a=iwpop2(); v=mgetx(a); wpshx(v); break; + /* STA */ case 0x0D: a=iwpop2(); v=wpopx(); msetx(a,v); break; + /* LDD */ case 0x0E: p=iwpop1(); v=dgetx(p); wpshx(v); break; + /* STD */ case 0x0F: p=iwpop1(); v=wpopx(); s=dsetx(p,v); if (s) { return s; } break; + /* ADD */ case 0x10: y=iwpopx(); x=wpopx(); wpshx(x+y); break; + /* SUB */ case 0x11: y=iwpopx(); x=wpopx(); wpshx(x-y); break; + /* INC */ case 0x12: x=iwpopx(); wpshx(x+1); break; + /* DEC */ case 0x13: x=iwpopx(); wpshx(x-1); break; + /* LTH */ case 0x14: y=iwpopx(); x=wpopx(); wpsh1(0-(x <y)); break; + /* GTH */ case 0x15: y=iwpopx(); x=wpopx(); wpsh1(0-(x >y)); break; + /* EQU */ case 0x16: y=iwpopx(); x=wpopx(); wpsh1(0-(x==y)); break; + /* NQK */ case 0x17: y=iwpopx(); x=wpopx(); wpshx(x); wpshx(y); wpsh1(0-(x!=y)); break; + /* SHL */ case 0x18: y=iwpop1(); x=wpopx(); wpshx(shlx(x,y)); break; + /* SHR */ case 0x19: y=iwpop1(); x=wpopx(); wpshx(shrx(x,y)); break; + /* ROL */ case 0x1A: y=iwpop1(); x=wpopx(); wpshx(rolx(x,y)); break; + /* ROR */ case 0x1B: y=iwpop1(); x=wpopx(); wpshx(rorx(x,y)); break; + /* IOR */ case 0x1C: y=iwpopx(); x=wpopx(); wpshx(x|y); break; + /* XOR */ case 0x1D: y=iwpopx(); x=wpopx(); wpshx(x^y); break; + /* AND */ case 0x1E: y=iwpopx(); x=wpopx(); wpshx(x&y); break; + /* NOT */ case 0x1F: x=iwpopx(); wpshx(~x ); break; } } - }; + return Signal.BREAK; + } } -// Stack for the core system. +// Stack implementation for the JavaScript core. function Stack() { this.mem = new Uint8Array(256); this.p = 0; @@ -1380,9 +836,222 @@ function Stack() { this.psh2 = (v) => { this.psh1(v >> 8); this.psh1(v); } this.pop1 = ( ) => { return this.mem[--this.p]; } this.pop2 = ( ) => { return this.pop1() | this.pop1() << 8; } + +} + + + +// ----------------------------------------------------------------------------------------------- + +// :::::: EMULATOR :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + +// ----------------------------------------------------------------------------------------------- + + + +// Signal enum for devices. +const Signal = Object.freeze({ + BREAK: 0, + HALT: 1, + DB1: 2, + DB2: 3, + DB3: 4, + DB4: 5, + DB5: 6, + DB6: 7, + SLEEP: 8, + RESET: 9, + FORK: 10, +}); + + +// Main Bedrock emulator implementation. +function Bedrock(emulatorElement, wasm=defaultToWasm) { + let core; + if (wasmModule && wasm) { + console.log('Instantiating Bedrock with WASM core'); + core = new BedrockWasm(); + } else { + console.log('Instantiating Bedrock with JS core'); + core = new BedrockJs(); + } + + this.e = emulatorElement; // the linked EmulatorElement + this.dev = new DeviceBus(this); + + this.init = async () => { + await core.init(this.dev.read, this.dev.write); + } + + // Fully reset the emulator, and load in a new program. + this.load = (bytecode) => { + this.reset(); + core.load(bytecode); + this.blank = false; + } + + // Fully reset the emulator. + this.reset = () => { + core.reset(); + this.dev.reset(); + this.blank = true; // emulator has been fully reset, screen is blank. + this.paused = true; // program is not currently executing. + this.asleep = false; // program is waiting for input. + this.halted = false; // current program has ended. + this.frameMark = 0; // time when previous frame was rendered. + } + + this.update = () => { + if (this.e) { + this.e.updateDOM(); + } + } + + this.render = () => { + if (this.e) { + if (this.dev.input.accessed || this.dev.screen.accessed) { + this.e.showScreenPanel(); + } + let scr = this.dev.screen; + if (scr.dirty()) { + scr.render(); + let image = new ImageData(scr.pixels, scr.width, scr.height); + scr.ctx.putImageData(image, 0, 0, scr.dx0, scr.dy0, scr.dx1, scr.dy1); + scr.unmark(); + this.frameMark = performance.now(); + } + } + } + + // Start or unpause the program. + this.run = () => { + this.paused = false; + this.update(); + this.runLoop(); + } + + // Pause the program, hold the current emulator state. + this.pause = () => { + this.paused = true; + this.update(); + this.render(); + } + + // End and reset the program, allow it to run from the beginning. + this.stop = () => { + this.reset(); + this.update(); + this.render(); + } + + this.sleep = () => { + this.asleep = true; + this.update(); + this.render(); + this.sleepLoop(); + } + + // End the program, hold the final emulator state. + this.halt = () => { + this.halted = true; + this.update(); + this.render(); + } + + // Run the program for a single cycle. + this.step = () => { + this.blank = false; + if (this.paused && !this.halted) { + if (this.asleep) { + setTimeout(this.sleepLoop.bind(this), 0); + return; + } + switch (core.eval(1)) { + case Signal.HALT: this.halt(); return; + case Signal.DB4: this.assert(); break; + // case Signal.RESET: this.reset(); break; + // case Signal.FORK: this.reset(); break; + case Signal.SLEEP: this.sleep(); return; + case Signal.DB1: this.pause(); break; + default: // NORMAL + } + this.update(); + this.render(); + } + } + + // Run the program indefinitely. + this.runLoop = () => { + if (!this.halted && !this.paused) { + if (this.asleep) { + setTimeout(this.sleepLoop.bind(this), 0); + return; + } + // Tail-recursive with timeout to allow other code to run. + switch (core.eval(cyclesPerBatch)) { + case Signal.HALT: this.halt(); return; + case Signal.DB4: this.assert(); break; + // case Signal.RESET: this.reset(); break; + // case Signal.FORK: this.reset(); break; + case Signal.SLEEP: this.sleep(); return; + case Signal.DB1: this.pause(); break; + default: // NORMAL + } + this.update(); + // Force a render if the frame has been running for too long. + if (performance.now() - this.frameMark > maxFrameTime) { + this.render(); + } + setTimeout(this.runLoop.bind(this), 0); + } + } + + // Wait for input before returning to runLoop. + this.sleepLoop = () => { + this.frameMark = performance.now(); + if (!this.halted) { + if (!this.asleep) { + setTimeout(this.runLoop.bind(this), 0); + return; + } + let queue = this.dev.queue.get(this.dev.system.wakeMask); + for (let slot of queue) { + if (this.dev.devices[slot].wake()) { + this.asleep = false; + this.dev.system.wakeSlot = slot; + setTimeout(this.runLoop.bind(this)); + return; + } + } + setTimeout(this.sleepLoop.bind(this), 0); + } + } + + this.debugState = () => { + let wst = []; let wp = core.wp(); + let rst = []; let rp = core.rp(); + for (let i=0; i<wp; i++) wst.push(core.wst(i)); + for (let i=0; i<rp; i++) rst.push(core.rst(i)); + return { + cc: core.cc(), + ip: core.ip(), + wst, rst, + }; + } + + this.assert = () => { + if (core.wp() == 1 && core.rp() == 0 && core.wst(0) == 0xFF) { + core.std(0x86, 0x2E); // period + } else { + core.std(0x86, 0x58); // capital X + } + } } + +// ----------------------------------------------------------------------------------------------- + +// :::::: EMULATOR DEVICES :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + +// ----------------------------------------------------------------------------------------------- + + + // Device bus for the core system. function DeviceBus(br) { this.system = new SystemDevice(br); @@ -1414,12 +1083,13 @@ function DeviceBus(br) { this.queue = new WakeQueue(); - this.reset = function() { + this.reset = () => { for (let d of this.devices) d.reset(); } - this.read = function(p) { + this.read = (p) => { return this.devices[p >> 4].read(p & 0x0F); } - this.write = function(p,v) { - return this.devices[p >> 4].write(p & 0x0F, v); } + this.write = (p,v) => { + return this.devices[p >> 4].write(p & 0x0F, v); + } } @@ -1474,8 +1144,8 @@ function SystemDevice(br) { this.write = function(p,v) { switch (p) { case 0x0: this.wakeMask = setH(this.wakeMask, v); break; - case 0x1: this.wakeMask = setL(this.wakeMask, v); return 2; - case 0x3: let s = v ? 3 : 4; return s; + case 0x1: this.wakeMask = setL(this.wakeMask, v); return Signal.SLEEP; + case 0x3: let s = v ? Signal.FORK : Signal.RESET; return s; case 0x8: this.nameBuffer.reset(); break; case 0x9: this.authorsBuffer.reset(); break; default: return; @@ -1772,8 +1442,8 @@ function InputDevice(br) { this.write = function(p,v) { this.accessed = true; switch (p) { - case 0xA: this.characterBytes = []; return; - default: return; + case 0xA: this.characterBytes.clear(); return; + default: return; } } this.wake = function() { @@ -1882,7 +1552,7 @@ function InputDevice(br) { function ScreenDevice(br) { this.init = function() { - this.ctx = br.e.canvas.getContext("2d", { + this.ctx = br.e.el.canvas.getContext("2d", { alpha: false, willReadFrequently: false, }); @@ -1903,7 +1573,7 @@ function ScreenDevice(br) { this.paletteW = 0; this.selection = [0, 0, 0, 0]; this.sprite = new SpriteBuffer(); - let initialWidth = Math.trunc(br.e.clientWidth / initialScreenScale); + let initialWidth = Math.trunc(br.e.el.clientWidth / initialScreenScale); let initialHeight = Math.trunc(initialWidth * 9/16); this.width = -1; // screen won't resize if dimensions aren't different this.resize(initialWidth, initialHeight, initialScreenScale); @@ -2173,10 +1843,10 @@ function StreamDevice(br) { } this.write = function(p,v) { switch (p) { - case 0x3: br.e.endTransmission(); break; + case 0x3: br.e.endTransmission(); break; case 0x6: - case 0x7: br.e.receiveTransmissionByte(v); break; - default: return; + case 0x7: br.e.receiveTransmissionByte(v); break; + default: return; } } this.wake = function() { @@ -2201,6 +1871,7 @@ function ClipboardDevice(br) { } this.write = function(p,v) { // HACK: The -1 is to break the run loop to allow the br.pause() to take hold. + // A 0 wouldn't be raised, and the higher signals have other meanings. switch (p) { case 0x2: this.readEntry(v); return -1; case 0x3: this.writeEntry(v); return -1; @@ -2217,7 +1888,6 @@ function ClipboardDevice(br) { this.readEntry = function(v) { this.readQueue = []; if (v) { - // TODO: Figure out how to make this synchronous. // HACK: Pause the emulator until promise resolves. if (navigator.clipboard.readText) { br.pause(); @@ -2238,20 +1908,13 @@ function ClipboardDevice(br) { } this.writeEntry = function(v) { if (v) { - // TODO: Figure out how to make this synchronous. // HACK: Pause the emulator until promise resolves. if (navigator.clipboard.writeText) { br.pause(); let parser = new Utf8StreamParser(); parser.pushList(this.writeQueue); - navigator.clipboard.writeText(parser.read()).then( - () => { - br.run(); - }, - () => { - br.run(); - } - ); + navigator.clipboard.writeText(parser.read()) + .finally(() => { br.run() }); } } else { // TODO: Look into valid clipboard binary formats. @@ -2436,12 +2099,667 @@ function signed(v) { return v << 16 >> 16; } function clamp(v,a,b) { return v>=a ? v<b ? v : b : a; } // inclusive a and b +// Incrementally parse a UTF-8 string. +function Utf8StreamParser() { + // Full characters parsed so far. + this.string = ''; + // Current partial code point. + this.codePoint; + // True if the current code point is being dropped. + this.dropBytes = false; + // Number of bytes received of the current code point. + this.i = 0; + // Expected byte length of the current code point. + this.len = 0; + + // Push a code point to the string. + let pushCodePoint = (codePoint) => { + this.string += String.fromCodePoint(codePoint); + } + // Drop the current partial code point, if any. + let dropCodePoint = () => { + // Push a 'replacement character' if there is a partial code point + // that hasn't already been dropped. + if (this.i && !this.dropBytes) pushCodePoint(0xFFFD); + this.codePoint = 0; + this.dropBytes = false; + this.i = 0; + this.len = 0; + } + // Parse the start of an encoded code point. + let parseStartByte = (byte, length) => { + dropCodePoint(); + this.len = length; + parseContinuationByte(byte); + } + // Parse the remaining bytes of an encoded code point. + let parseContinuationByte = (byte) => { + if (!this.dropBytes) { + if (this.len) { + this.codePoint = this.codePoint << 6 | (byte & 0x3F); + this.i += 1; + if (this.i == this.len) { + pushCodePoint(this.codePoint); + this.len = 0; + } + } else { + this.dropBytes = true; + pushCodePoint(0xFFFD); + } + } + } + + // Parse a UTF-8 encoded byte. + this.push = (byte) => { + if (byte < 0x80) { // start of a 1-byte code point + dropCodePoint(); + pushCodePoint(byte); + } else if (byte < 0xC0) { // not a start byte + parseContinuationByte(byte); + } else if (byte < 0xE0) { // start of a 2-byte code point + parseStartByte(byte, 2); + } else if (byte < 0xF0) { // start of a 3-byte code point + parseStartByte(byte, 3); + } else if (byte < 0xF8) { // start of a 4-byte code point + parseStartByte(byte, 4); + } + } + // Parse an array of bytes. + this.pushList = (list) => { + for (let byte of list) this.push(byte); + } + + // Take the characters that have been parsed so far. + this.read = () => { + let output = this.string; + this.string = ''; + return output; + } +} + + + +// ----------------------------------------------------------------------------------------------- + +// :::::: ASSEMBLER ELEMENT ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + +// ----------------------------------------------------------------------------------------------- + + + +// Constructor for an interactive assembler element. +function AssemblerElement(element) { + this.el = instantiateFromTemplate(assemblerTemplate); + this.el.textarea = this.el.querySelector('textarea'); + this.el.viewer = this.el.querySelector('.viewer'); + this.el.editor = this.el.querySelector('.editor'); + this.el.errorPanel = this.el.querySelector('.panel.errors'); + this.el.bytecodePanel = this.el.querySelector('.panel.bytecode'); + this.el.status = this.el.querySelector('.status'); + this.el.checkButton = this.el.querySelector('button[name="check"]'); + this.el.runButton = this.el.querySelector('button[name="run"]'); + this.el.fullscreenButton = this.el.querySelector('button[name="fullscreen"]'); + + // Copy the text content of a passed element into the editor. + if (element) { + this.el.textarea.value = element.textContent.trim(); + this.el.textarea.style.height = element.clientHeight + 'px'; + } + + this.autoGrow = true; // increase height as text is entered + this.emulator = null; // linked emulator element, if any + + // Show and hide the error and bytecode panels. + this.showErrorPanel = () => { + this.hideBytecodePanel(); + this.el.errorPanel.classList.remove('hidden'); } + this.hideErrorPanel = () => { + this.el.errorPanel.classList.add('hidden'); } + this.showBytecodePanel = () => { + this.hideErrorPanel(); + this.el.bytecodePanel.classList.remove('hidden'); } + this.hideBytecodePanel = () => { + this.el.bytecodePanel.classList.add('hidden'); } + + // Re-align the viewer to the textarea by setting the viewer scroll position. + this.syncScroll = () => { + this.el.viewer.scrollTop = this.el.textarea.scrollTop; + this.el.viewer.scrollLeft = this.el.textarea.scrollLeft; + } + + // Re-align the viewer to the textarea by setting the viewer height. + this.syncHeight = () => { + // Disable auto-grow if editor was shrunk by the user. + if (this.el.textarea.clientHeight < this.el.viewer.clientHeight) { + this.autoGrow = false; } + let height = this.el.textarea.clientHeight + 'px'; + this.el.viewer.style.height = height; + this.el.editor.style.height = height; + } + + // Copy text from textarea to viewer with syntax highlighting. + this.renderText = () => { + // HACK: Force viewer to show trailing newlines. + let text = this.el.textarea.value; + if (text.slice(-1) == "\n") text += ' '; + // Replace contents of viewer. + this.el.viewer.innerHTML = ''; + for (let node of highlightSource(text)) { + this.el.viewer.appendChild(node); } + // Auto-grow the text area as text is entered. + if (this.autoGrow && this.el.textarea.scrollHeight > this.el.textarea.clientHeight) { + this.el.textarea.style.height = this.el.textarea.scrollHeight + 'px'; } + // Lock viewer to textarea. + this.syncScroll(); + } + + // Assemble the program and show bytecode or errors. + this.checkProgram = () => { + let { bytecode, symbols, errors } = assembleSource(this.el.textarea.value); + this.el.errorPanel.innerHTML = ''; + this.el.bytecodePanel.innerHTML = ''; + if (errors.length) { + let list = document.createElement('ul'); + for (let error of errors) { + let item = document.createElement('li'); + let line = error.start.line + 1; + let column = error.start.column + 1; + item.textContent = `[${line}:${column}] ${error.text}`; + list.appendChild(item); } + this.el.errorPanel.appendChild(list); + let unit = errors.length == 1 ? 'error' : 'errors'; + this.el.status.textContent = `${errors.length} ${unit}`; + this.showErrorPanel(); + } else { + let unit = bytecode.length == 1 ? 'byte' : 'bytes'; + this.el.status.textContent = `no errors (${bytecode.length} ${unit})`; + let programListing = document.createElement('div'); + programListing.textContent = hexArray(bytecode); + this.el.bytecodePanel.appendChild(programListing); + this.hideErrorPanel(); + this.hideBytecodePanel(); + if (bytecode.length) this.showBytecodePanel(); + return { bytecode, symbols }; + } + } + + // Assemble the program and run in an emulator. + this.runProgram = () => { + // Check and assemble program. + let program = this.checkProgram(); + if (program) { + this.hideBytecodePanel(); + let { bytecode, symbols } = program; + if (this.emulator) { + this.emulator.showStatePanel(); + this.emulator.startProgram(bytecode, symbols); + } else { + // Instantiate a new emulator element if none exists. + this.emulator = new EmulatorElement(); + this.emulator.assembler = this; + this.el.parentElement.insertBefore(this.emulator.el, this.el.nextSibling); + this.emulator.init().then(() => { + this.emulator.showStatePanel(); + this.emulator.startProgram(bytecode, symbols); + }) + } + } + } + + // Toggle the fullscreen state of the whole assembler element. + this.toggleFullscreen = () => { + if (document.fullscreenElement != this.el) { + this.el.requestFullscreen(); + } else { + document.exitFullscreen(); + } + } + + // Handle keypresses on the whole assembler. + this.onKeyPress = (event) => { + if (event.key == "Escape") { + event.preventDefault(); + this.hideErrorPanel(); + this.hideBytecodePanel(); + } else if (event.key == "s" && event.ctrlKey) { + event.preventDefault(); + this.checkProgram(); + } + } + + // Handle keypresses on just the editor. + this.onEditorKeyPress = (event) => { + let indent = ' '; + let text = this.el.textarea.value; + let selectionStart = this.el.textarea.selectionStart; + let selectionEnd = this.el.textarea.selectionEnd; + + // Find the character index following the previous newline character. + function findLineStart(i) { + while (i>0 && text[i-1] != '\n') i -= 1; + return i; } + + if (event.key == "Enter" && event.ctrlKey) { + event.preventDefault(); + this.runProgram(); + } else if (event.key == "Tab") { + event.preventDefault(); + // Find the start index of every line in the selection. + let i = findLineStart(selectionStart); + let lineStarts = [i]; + for (; i<selectionEnd; i++) if (text[i] == '\n') lineStarts.push(i+1); + if (event.shiftKey) { + // Find length of the leading whitespace in each line. + let whitespaceLengths = []; + for (let start of lineStarts) { + let end = start; + while ('\t '.includes(text[end])) end++; + whitespaceLengths.push(end-start); } + // Unindent each line in reverse order. + selectionStart -= Math.min(tabSize, whitespaceLengths[0]); + for (let i=lineStarts.length-1; i>=0; i--) { + let start = lineStarts[i]; + let trim = Math.min(tabSize, whitespaceLengths[i]); + text = text.slice(0, start) + text.slice(start+trim); + selectionEnd -= trim; } + } else { + // Indent each line in reverse order. + selectionStart += tabSize; + for (let i=lineStarts.length-1; i>=0; i--) { + let start = lineStarts[i]; + text = text.slice(0, start) + indent + text.slice(start); + selectionEnd += tabSize; } + } + // Update textarea content and viewer. + this.el.textarea.value = text; + this.el.textarea.selectionStart = selectionStart; + this.el.textarea.selectionEnd = selectionEnd; + this.renderText(); + } + } + + this.el.textarea.addEventListener('input', this.renderText); + this.el.textarea.addEventListener('scroll', this.syncScroll); + this.el.textarea.addEventListener('keydown', this.onEditorKeyPress); + this.el.addEventListener('keydown', this.onKeyPress); + this.el.checkButton.addEventListener('click', this.checkProgram); + this.el.runButton.addEventListener('click', this.runProgram); + this.el.fullscreenButton.addEventListener('click', this.toggleFullscreen); + + this.el.resizeObserver = new ResizeObserver(this.syncHeight).observe(this.el.textarea); + this.el.textarea.selectionStart = 0; + this.el.textarea.selectionEnd = 0; + this.el.textarea.scrollLeft = 0; + this.renderText(); +} + + + +// ----------------------------------------------------------------------------------------------- + +// :::::: EMULATOR ELEMENT :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + +// ----------------------------------------------------------------------------------------------- + + + +/// Constructor for a interactive emulator element. +function EmulatorElement(options) { + this.el = instantiateFromTemplate(emulatorTemplate); + // Menu bar + this.el.menuBar = this.el.querySelector('.menubar'); + this.el.fullscreenButton = this.el.menuBar.querySelector('button[name="fullscreen"]'); + this.el.stateButton = this.el.menuBar.querySelector('button[name="state"]'); + this.el.runButton = this.el.menuBar.querySelector('button[name="run"]'); + this.el.pauseButton = this.el.menuBar.querySelector('button[name="pause"]'); + this.el.stopButton = this.el.menuBar.querySelector('button[name="stop"]'); + this.el.stepButton = this.el.menuBar.querySelector('button[name="step"]'); + this.el.status = this.el.menuBar.querySelector('.status'); + // Screen panel + this.el.screenPanel = this.el.querySelector('.screen'); + this.el.canvas = this.el.screenPanel.querySelector('canvas'); + // Stream panel + this.el.streamPanel = this.el.querySelector('.stream'); + this.el.transmissions = this.el.querySelector('.transmissions'); + // State panel + this.el.statePanel = this.el.querySelector('.panel.state'); + this.el.ipValue = this.el.statePanel.querySelector('#ip .value'); + this.el.ipNote = this.el.statePanel.querySelector('#ip .note'); + this.el.ccValue = this.el.statePanel.querySelector('#cc .value'); + this.el.ccNote = this.el.statePanel.querySelector('#cc .note'); + this.el.wstValue = this.el.statePanel.querySelector('#wst .value'); + this.el.rstValue = this.el.statePanel.querySelector('#rst .value'); + + let withWasm = options ? options.wasm : undefined; + this.br = new Bedrock(this, withWasm); + this.bytecode = options ? options.bytecode : new Uint8Array(0); + this.symbols = new SymbolTable(); + + // Buffered transmission handling for stream device. + let currentTransmission = null; + let transmissionParser = new Utf8StreamParser(); + + // Initialise emulator and emulator element. + this.init = async () => { + await this.br.init(); + this.br.reset(); + this.br.onUpdate = () => this.updateDOM(); + + // Apply options. + if (options && !options.controls) this.hideMenuBar(); + if (options && options.autoplay) this.runProgram(); + if (options && options.nocursor) this.el.canvas.style.cursor = 'none'; + + // Make canvas focusable to be able to receive keydown and keyup events. + this.el.canvas.tabIndex = 0; + + this.el.fullscreenButton.addEventListener('click', this.toggleFullscreen); + this.el.stateButton.addEventListener('click', this.toggleStatePanel); + this.el.runButton.addEventListener('click', this.runProgram); + this.el.pauseButton.addEventListener('click', this.pauseProgram); + this.el.stopButton.addEventListener('click', this.stopProgram); + this.el.stepButton.addEventListener('click', this.br.step); + this.resizeObserver = new ResizeObserver(this.updateScreenSize).observe(this.el.screenPanel); + + this.el.canvas.addEventListener('mousemove', this.mouseMove); + this.el.canvas.addEventListener('pointermove', this.mouseMove); + this.el.canvas.addEventListener('mousedown', this.mouseDown); + this.el.canvas.addEventListener('mouseup', this.mouseUp); + this.el.canvas.addEventListener('touchstart', this.touchStart); + this.el.canvas.addEventListener('touchend', this.touchEnd); + this.el.canvas.addEventListener('touchcancel', this.touchEnd); + this.el.canvas.addEventListener('mouseenter', this.mouseEnter); + this.el.canvas.addEventListener('mouseleave', this.mouseExit); + this.el.canvas.addEventListener('wheel', this.mouseScroll); + this.el.canvas.addEventListener('contextmenu', (e)=>{e.preventDefault()}); + this.el.canvas.addEventListener('keydown', (e)=>this.keyInput(e, true)); + this.el.canvas.addEventListener('keyup', (e)=>this.keyInput(e, false)); + } + + this.showStatePanel = () => { + this.el.statePanel.classList.remove('hidden'); } + this.toggleStatePanel = () => { + this.el.statePanel.classList.toggle('hidden'); } + this.showStreamPanel = () => { + this.el.streamPanel.classList.remove('hidden'); } + this.hideStreamPanel = () => { + this.el.streamPanel.classList.add('hidden'); } + this.showScreenPanel = () => { + this.el.screenPanel.classList.remove('hidden'); } + this.hideScreenPanel = () => { + this.el.screenPanel.classList.add('hidden'); } + this.hideMenuBar = () => { + this.el.menuBar.classList.add('hidden'); } + + // Reset the emulator, load and run a new program. + this.startProgram = (bytecode, symbols) => { + this.stopProgram(); + this.bytecode = bytecode; + this.symbols = symbols; + this.br.load(bytecode); + this.br.run(); + } + + // Fires when the stop button is pressed. + this.stopProgram = () => { + this.br.stop(); + this.updateDOM(); + this.el.transmissions.innerHTML = ''; + this.hideStreamPanel(); + this.hideScreenPanel(); + currentTransmission = null; + } + + // Fires when the play button is pressed. + this.runProgram = () => { + if (this.br.blank) { + this.startProgram(this.bytecode, this.symbols); + } else { + this.br.run(); + } + } + + // Fires when the pause button is pressed. + this.pauseProgram = () => { + this.br.pause(); + } + + // Update information displayed in the emulator frame. + this.updateDOM = () => { + let { cc, ip, wst, rst } = this.br.debugState(); + this.el.ipValue.textContent = hex(ip, 4); + this.el.ipNote.textContent = this.symbols.findSymbol(ip); + this.el.ccValue.textContent = cc; + this.el.wstValue.textContent = hexArray(wst); + this.el.rstValue.textContent = hexArray(rst); + if (this.br.halted) { + this.el.status.textContent = 'ended'; + } else if (this.br.blank) { + this.el.status.textContent = ''; + } else if (this.br.paused) { + this.el.status.textContent = 'paused'; + } else if (this.br.asleep) { + let wakeMask = this.br.dev.system.wakeMask; + this.el.status.textContent = `sleeping / 0x${hex(wakeMask, 4)}`; + } else { + this.el.status.textContent = `running / ${cc}`; + } + this.flushTransmission(); + } + + this.updateScreenSize = () => { + if (!this.el.screenPanel.classList.contains('hidden')) { + if (document.fullscreenElement == this.el) { + let width = this.el.screenPanel.clientWidth; + let height = this.el.screenPanel.clientHeight; + this.br.dev.screen.resizeToFill(width, height); + } else { + let width = this.el.screenPanel.clientWidth; + let height = width * 9/16; + this.br.dev.screen.resizeToFill(width, height); + } + } + } + + // Receive an incoming byte from the local stream. + this.receiveTransmissionByte = (byte) => { + transmissionParser.push(byte); + } + + // Push all received bytes to the DOM. + this.flushTransmission = () => { + let string = transmissionParser.read(); + if (string) { + if (!currentTransmission) { + this.showStreamPanel(); + let element = document.createElement('li'); + element.addEventListener('click', () => { + copyText(element.textContent); }) + currentTransmission = element; + this.el.transmissions.appendChild(element); + // Prune old transmissions. + let count = Math.max(this.el.transmissions.children.length - maxTransmissions, 0); + for (let i=0; i<count; i++) this.el.transmissions.children[0].remove(); + } + currentTransmission.textContent += string; + this.el.streamPanel.scrollTop = this.el.streamPanel.scrollHeight; + } + } + + // End the current incoming transmission. + this.endTransmission = () => { + this.flushTransmission(); + currentTransmission = null; + } + + // Change the fullscreen state of the whole emulator. + this.toggleFullscreen = () => { + if (document.fullscreenElement != this.el) { + this.el.requestFullscreen(); + } else { + document.exitFullscreen(); + } + } + + this.mouseMove = (e) => { + let bounds = this.el.canvas.getBoundingClientRect(); + let scale = this.br.dev.screen.scale; + let x = ((e.clientX - bounds.left) / scale) & 0xFFFF; + let y = ((e.clientY - bounds.top ) / scale) & 0xFFFF; + this.br.dev.input.applyPosition(x, y); + } + + this.mouseDown = (e) => { this.br.dev.input.applyButtons(e.buttons); } + this.mouseUp = (e) => { this.br.dev.input.applyButtons(e.buttons); } + + this.mouseEnter = (e) => { + this.br.dev.input.applyActive(true); + this.mouseMove(e); + } + + this.mouseExit = (e) => { + // HACK: Force buttons to release when the mouse leaves the screen, + // otherwise any buttons that were held down when the mouse leaves + // will be stuck down when the mouse re-enters, even if they were + // released off-screen. + // TODO: Figure out how to attach a listener to the window as a whole + // that can tell each emulator how the mouse behaves off-screen, so + // that the mouse can be dragged off and onto a window in one + // continuous stroke. + this.br.dev.input.applyButtons(0x00); + this.br.dev.input.applyActive(false); + this.mouseMove(e); + } + + this.mouseScroll = (e) => { + // Only capture scroll events if canvas is focused. + if (document.activeElement == this.el.canvas) { + e.preventDefault(); + let scale; + // TODO: Dial these numbers in a bit. + switch (e.deltaMode) { + case (e.DOM_DELTA_PIXEL): scale = 1/10; break; + case (e.DOM_DELTA_LINE): scale = 1; break; + case (e.DOM_DELTA_PAGE): scale = 20; break; + }; + this.br.dev.input.applyHScroll(e.deltaX * scale); + this.br.dev.input.applyVScroll(e.deltaY * scale); + } + } + + this.keyInput = (e, pressed) => { + e.preventDefault(); + this.br.dev.input.applyModifiers(e); + if (!e.repeat && !e.isComposing) { + this.br.dev.input.applyKey(e, pressed); + } + } + + this.touchStart = (e) => { + if (e.changedTouches.length) { + this.mouseMove(e.changedTouches[0]); } + this.br.dev.input.applyActive(true); + this.br.dev.input.applyButtons(0x01); + } + + this.touchEnd = (e) => { + this.br.dev.input.applyActive(false); + this.br.dev.input.applyButtons(0x00); + } +} + + + +// ----------------------------------------------------------------------------------------------- + +// :::::: METADATA :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + +// ----------------------------------------------------------------------------------------------- + + + +function parseMetadata(bytecode) { + // Test identifier to see if program has metadata. + let id = [0xE8,0x00,0x18,0x42,0x45,0x44,0x52,0x4F,0x43,0x4B]; + for (let i=0; i<10; i++) if (bytecode[i] != id[i]) return; + // Parse each metadata item. + let name = getString(0x000A); + let authors = getString(0x000C); + let description = getString(0x000E); + let bgColour = getColour(0x0010); + let fgColour = getColour(0x0012); + let smallIcon = getIcon(0x0014, 24); + let largeIcon = getIcon(0x0016, 64); + return { name, authors, description, bgColour, fgColour, smallIcon, largeIcon }; + + function getByte(addr) { + return bytecode[addr] || 0; + } + function getDouble(addr) { + return getByte(addr) << 8 | getByte(addr+1); + } + function getString(addr) { + addr = getDouble(addr); + if (addr) { + let parser = new Utf8StreamParser(); + while (true) { + let byte = bytecode[addr++]; + if (!byte) break; + parser.push(byte); + } + return parser.read(); + } + } + function getColour(addr) { + addr = getDouble(addr); + if (addr) { + let colour = getDouble(addr); + let r = (colour >> 8 & 0xF) * 17; + let g = (colour >> 8 & 0xF) * 17; + let b = (colour >> 8 & 0xF) * 17; + return [r, g, b]; + } + } + function getIcon(addr, size) { + // TODO: Return a custom type containing the dimensions and raw + // pixel data, and a colourise method to generate a coloured ImageData. + } +} + + // ----------------------------------------------------------------------------------------------- + -// :::::: CONSTANTS ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + +// :::::: UTILITIES ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + // ----------------------------------------------------------------------------------------------- + +// Convert an integer to a hexadecimal string. +function hex(value, pad=2) { + return value.toString(16).toUpperCase().padStart(pad, '0'); +} + +// Convert a whole byte array to a hexadecimal string. +function hexArray(array) { + let string = ''; + for (let byte of array) string += hex(byte) + ' '; + return string; +} + +// Instantiate an element from an HTML string. +function instantiateFromTemplate(html) { + let template = document.createElement('template'); + template.innerHTML = html.trim(); + return template.content.firstChild; +} + +// Copy a string to the clipboard. +function copyText(text) { + navigator.clipboard.writeText(text); +} + +let encoder = new TextEncoder(); +function encodeUtf8(text) { + return encoder.encode(text); +} + + +// ----------------------------------------------------------------------------------------------- + +// :::::: HTML CSS SVG :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + +// ----------------------------------------------------------------------------------------------- + + + +// Link to the bedrock-js project page on benbridle.com. Includes the bedrock logo as an SVG. const projectLink = ` <a href='https://benbridle.com/bedrock-js' target='_blank'> <svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8' width='8' height='8'> @@ -2507,7 +2825,6 @@ const assemblerTemplate = ` </div> `; - const emulatorTemplate = ` <div class='bedrock emulator'> <div class='menubar'> @@ -2521,17 +2838,17 @@ const emulatorTemplate = ` ${projectLink} </div> <div class='hidden panel state'><ul> - <li>PC: <span class='pc'></span> <span class='label'></span></li> - <li>WST: <span class='wst'></span></li> - <li>RST: <span class='rst'></span></li> + <li id='ip' > IP: <span class='value'></span> <span class='note'></span></li> + <li id='cc' > CC: <span class='value'></span> <span class='note'></span></li> + <li id='wst'>WST: <span class='value'></span></li> + <li id='rst'>RST: <span class='value'></span></li> </ul></div> <div class='hidden stream'><ul class='transmissions'></ul></div> <div class='hidden screen'><canvas></canvas></div> </div> `; - -const bedrockStyles = ` +const bedrockCSS = ` div.bedrock { --br-fg:var(--fg,#222); --br-bg:var(--bg,#eed); --br-accent:var(--accent,#467); } div.bedrock * { @@ -2586,7 +2903,7 @@ const bedrockStyles = ` overflow:scroll; margin:0; list-style:none; } .bedrock .panel li { white-space:pre; } - .bedrock .panel .label { + .bedrock .panel .note { opacity:60%; } .bedrock .stream { @@ -2606,43 +2923,3 @@ const bedrockStyles = ` .bedrock:fullscreen .stream { max-height:none; } `; - - -const instructionTable = { - 'HLT': 0x00, 'NOP' : 0x20, 'DB1' : 0x40, 'DB2' : 0x60, 'DB3' : 0x80, 'DB4' : 0xA0, 'DB5' : 0xC0, 'DB6' : 0xE0, - 'PSH': 0x01, 'PSH:': 0x21, 'PSH*': 0x41, 'PSH*:': 0x61, 'PSHr': 0x81, 'PSHr:': 0xA1, 'PSHr*': 0xC1, 'PSHr*:': 0xE1, - ':': 0x21, '*:': 0x61, 'r:': 0xA1, 'r*:': 0xE1, - 'POP': 0x02, 'POP:': 0x22, 'POP*': 0x42, 'POP*:': 0x62, 'POPr': 0x82, 'POPr:': 0xA2, 'POPr*': 0xC2, 'POPr*:': 0xE2, - 'CPY': 0x03, 'CPY:': 0x23, 'CPY*': 0x43, 'CPY*:': 0x63, 'CPYr': 0x83, 'CPYr:': 0xA3, 'CPYr*': 0xC3, 'CPYr*:': 0xE3, - 'DUP': 0x04, 'DUP:': 0x24, 'DUP*': 0x44, 'DUP*:': 0x64, 'DUPr': 0x84, 'DUPr:': 0xA4, 'DUPr*': 0xC4, 'DUPr*:': 0xE4, - 'OVR': 0x05, 'OVR:': 0x25, 'OVR*': 0x45, 'OVR*:': 0x65, 'OVRr': 0x85, 'OVRr:': 0xA5, 'OVRr*': 0xC5, 'OVRr*:': 0xE5, - 'SWP': 0x06, 'SWP:': 0x26, 'SWP*': 0x46, 'SWP*:': 0x66, 'SWPr': 0x86, 'SWPr:': 0xA6, 'SWPr*': 0xC6, 'SWPr*:': 0xE6, - 'ROT': 0x07, 'ROT:': 0x27, 'ROT*': 0x47, 'ROT*:': 0x67, 'ROTr': 0x87, 'ROTr:': 0xA7, 'ROTr*': 0xC7, 'ROTr*:': 0xE7, - 'JMP': 0x08, 'JMP:': 0x28, 'JMP*': 0x48, 'JMP*:': 0x68, 'JMPr': 0x88, 'JMPr:': 0xA8, 'JMPr*': 0xC8, 'JMPr*:': 0xE8, - 'JMS': 0x09, 'JMS:': 0x29, 'JMS*': 0x49, 'JMS*:': 0x69, 'JMSr': 0x89, 'JMSr:': 0xA9, 'JMSr*': 0xC9, 'JMSr*:': 0xE9, - 'JCN': 0x0A, 'JCN:': 0x2A, 'JCN*': 0x4A, 'JCN*:': 0x6A, 'JCNr': 0x8A, 'JCNr:': 0xAA, 'JCNr*': 0xCA, 'JCNr*:': 0xEA, - 'JCS': 0x0B, 'JCS:': 0x2B, 'JCS*': 0x4B, 'JCS*:': 0x6B, 'JCSr': 0x8B, 'JCSr:': 0xAB, 'JCSr*': 0xCB, 'JCSr*:': 0xEB, - 'LDA': 0x0C, 'LDA:': 0x2C, 'LDA*': 0x4C, 'LDA*:': 0x6C, 'LDAr': 0x8C, 'LDAr:': 0xAC, 'LDAr*': 0xCC, 'LDAr*:': 0xEC, - 'STA': 0x0D, 'STA:': 0x2D, 'STA*': 0x4D, 'STA*:': 0x6D, 'STAr': 0x8D, 'STAr:': 0xAD, 'STAr*': 0xCD, 'STAr*:': 0xED, - 'LDD': 0x0E, 'LDD:': 0x2E, 'LDD*': 0x4E, 'LDD*:': 0x6E, 'LDDr': 0x8E, 'LDDr:': 0xAE, 'LDDr*': 0xCE, 'LDDr*:': 0xEE, - 'STD': 0x0F, 'STD:': 0x2F, 'STD*': 0x4F, 'STD*:': 0x6F, 'STDr': 0x8F, 'STDr:': 0xAF, 'STDr*': 0xCF, 'STDr*:': 0xEF, - 'ADD': 0x10, 'ADD:': 0x30, 'ADD*': 0x50, 'ADD*:': 0x70, 'ADDr': 0x90, 'ADDr:': 0xB0, 'ADDr*': 0xD0, 'ADDr*:': 0xF0, - 'SUB': 0x11, 'SUB:': 0x31, 'SUB*': 0x51, 'SUB*:': 0x71, 'SUBr': 0x91, 'SUBr:': 0xB1, 'SUBr*': 0xD1, 'SUBr*:': 0xF1, - 'INC': 0x12, 'INC:': 0x32, 'INC*': 0x52, 'INC*:': 0x72, 'INCr': 0x92, 'INCr:': 0xB2, 'INCr*': 0xD2, 'INCr*:': 0xF2, - 'DEC': 0x13, 'DEC:': 0x33, 'DEC*': 0x53, 'DEC*:': 0x73, 'DECr': 0x93, 'DECr:': 0xB3, 'DECr*': 0xD3, 'DECr*:': 0xF3, - 'LTH': 0x14, 'LTH:': 0x34, 'LTH*': 0x54, 'LTH*:': 0x74, 'LTHr': 0x94, 'LTHr:': 0xB4, 'LTHr*': 0xD4, 'LTHr*:': 0xF4, - 'GTH': 0x15, 'GTH:': 0x35, 'GTH*': 0x55, 'GTH*:': 0x75, 'GTHr': 0x95, 'GTHr:': 0xB5, 'GTHr*': 0xD5, 'GTHr*:': 0xF5, - 'EQU': 0x16, 'EQU:': 0x36, 'EQU*': 0x56, 'EQU*:': 0x76, 'EQUr': 0x96, 'EQUr:': 0xB6, 'EQUr*': 0xD6, 'EQUr*:': 0xF6, - 'NQK': 0x17, 'NQK:': 0x37, 'NQK*': 0x57, 'NQK*:': 0x77, 'NQKr': 0x97, 'NQKr:': 0xB7, 'NQKr*': 0xD7, 'NQKr*:': 0xF7, - 'SHL': 0x18, 'SHL:': 0x38, 'SHL*': 0x58, 'SHL*:': 0x78, 'SHLr': 0x98, 'SHLr:': 0xB8, 'SHLr*': 0xD8, 'SHLr*:': 0xF8, - 'SHR': 0x19, 'SHR:': 0x39, 'SHR*': 0x59, 'SHR*:': 0x79, 'SHRr': 0x99, 'SHRr:': 0xB9, 'SHRr*': 0xD9, 'SHRr*:': 0xF9, - 'ROL': 0x1A, 'ROL:': 0x3A, 'ROL*': 0x5A, 'ROL*:': 0x7A, 'ROLr': 0x9A, 'ROLr:': 0xBA, 'ROLr*': 0xDA, 'ROLr*:': 0xFA, - 'ROR': 0x1B, 'ROR:': 0x3B, 'ROR*': 0x5B, 'ROR*:': 0x7B, 'RORr': 0x9B, 'RORr:': 0xBB, 'RORr*': 0xDB, 'RORr*:': 0xFB, - 'IOR': 0x1C, 'IOR:': 0x3C, 'IOR*': 0x5C, 'IOR*:': 0x7C, 'IORr': 0x9C, 'IORr:': 0xBC, 'IORr*': 0xDC, 'IORr*:': 0xFC, - 'XOR': 0x1D, 'XOR:': 0x3D, 'XOR*': 0x5D, 'XOR*:': 0x7D, 'XORr': 0x9D, 'XORr:': 0xBD, 'XORr*': 0xDD, 'XORr*:': 0xFD, - 'AND': 0x1E, 'AND:': 0x3E, 'AND*': 0x5E, 'AND*:': 0x7E, 'ANDr': 0x9E, 'ANDr:': 0xBE, 'ANDr*': 0xDE, 'ANDr*:': 0xFE, - 'NOT': 0x1F, 'NOT:': 0x3F, 'NOT*': 0x5F, 'NOT*:': 0x7F, 'NOTr': 0x9F, 'NOTr:': 0xBF, 'NOTr*': 0xDF, 'NOTr*:': 0xFF, - - // TODO: Remove. - '?':0x40, -}; |