I've been playing the idea of developing in a virtual machine in a virtual assembly language - I have even coded a pretty reasonable POC.
It works by coding your assembly language in a hybrid JS / Assembly language and you can run and debug it in your browser - no more crashing or annoying inside emulator development. The goal is to convert the virtual assembler to native assembler - which is a lot easier than a full blown compiler and we get better resulting code.
Any thoughts?
Registers:
There are a set of virtual registers which would map to real registers as closely as possible. Basically A8 to E8 for 8bit ones and A16 to E16 for 16 bit ones. Using the registers closest to A are more likely to be allocated to real registers than ones below depending on target platform.
// registers
//
// js 6502 z80
// == ==== ===
// A8, A8' A A
// B8, B8' X B
// C8, C8' Y C
// D8, D8' simulated simulated
// E8, E8' simulated simulated
// A16, A16' simulated HL
// B16, B16' simulated DE
// C16, C16' simulated IX
// D16, D16' simulated IY
// E16, E16' simulated simulated
// F, F' S F
// M, M' virtual virtual
// PC PC PC
// SP simulated SP
// flags
//
// js 6502 z80
// == ==== ===
// C c c
// I i simulated
// Z z z
Instruction Summary:
// instruction summary
//
// valid statuses: R = Simulator, 6 = 6502, Z = Z80
//
// status vasm 6502 z80
// ====== ==== ==== ===
// (general register movement instructions)
// R alt simulated exx
// R ld simulated ld
// lda,ldx,ldy
// sta,stx,sty
// tax, txa
// tay, tya
// tsx, txs
// R nop nop nop
// R pop pla,plp pop
// R push pha,php push
// R swap simulated ex & simulated
// (flow control instructions)
// R call jsr call
// R if(test( simulated simulated
// R return ret ret
// R while(test( simulated simulated
// (arithmetic instructions)
// R adc adc adc
// R add ? add
// R cp cmp,cpx,cpy cp
// R dec dex,dey dec
// PRIORITY2 decm dec simulated: ld hl,m; dec (hl)
// R div simulated simulated
// R inc inx,iny inc
// PRIORITY2 incm inc simulated: ld hl,m; inc (hl)
// R mod simulated simulated
// R mult simulated simulated
// R sbc sbc sbc
// R sub ? sub
// (input / output instructions)
// PRIORITY2 input simulated in
// R output simulated out
// (bit instructions)
// PRIORITY1 and and and
// PRIORITY2 bit bit bit
// PRIORITY1 or ora or
// PRIORITY3 rla rol rla
// PRIORITY3 rra ror rra
// R set(C clc simulated: and a
// R set(I sei di, ei
// R set(Z simulated simulated
// PRIORITY3 sla asl sla
// PRIORITY3 sra lsr sra
// PRIORITY1 xor eor xor
// (block instructions)
// PRIORITY4 cpd simulated cpd
// PRIORITY4 cpdr simulated cpdr
// PRIORITY4 cpi simulated cpi
// PRIORITY4 cpir simulated cpir
// PRIORITY4 ldd simulated ldd
// PRIORITY4 lddr simulated lddr
// PRIORITY4 ldi simulated ldi
// PRIORITY4 ldir simulated ldir
// replaced instructions: by while(test(, if(test( and return
// jp jmp jp
// jpnc bcc jp nc
// jpc bcs jp c
// jrnc bcc jr nc
// jrc bcs jr c
// jpnz bne jp nz
// jpz beq jp z
// jrnz bne jr nz
// jrz beq jr z
// reti rti reti
Example:
Working JavaScript, within the simulator at the moment I have setup output port 8 to be output a char to the console. It is quite obvious on a z80, what the below would assemble to.
function main()
{
ld(A8, 5);
set(Z, 0);
while(test(NZ))
{
call(outA8);
dec(A8);
}
}
function outA8()
{
output(1, A8);
}
call(main);
Challenges:
There have been only a couple of challenges so far but I think I have reasonably been able to get through them.
Addressing Modes:
Coming up with a method of implementing addressing modes in JS which translates optimally to both a z80 and 6502, but I am pretty happy with the result so far. That is as you would usually expect, but we have 3 addressing modifiers: M for memory, H for high byte and L for low byte e.g.
Summary of ld commands:
// A:r8 -v ld(<R8>, <value>); ld a, 100
// B:r8 -r8 ld(<R8>, <R8>); ld a, b
// C:r8 -mv ld(<R8>, M(<value>)); ld a, (100)
// D:r8 -mr16 ld(<R8>, M(<R16>)); ld a, (hl)
// E:r8 -l ld(<R8>, L(<R16>)); ld a, l
// F:r8 -h ld(<R8>, H(<R16>)); ld a, h
// G:r16-v ld(<R16>, <value>); ld hl, 100
// H:r16-f ld(<R16>, <function>); ld hl, function1
// I:r16-r16 ld(<R16>, <R16>); ld hl, de
// J:r16-mv ld(<R16>, M(<value>)); ld hl, (100)
// K:r16-mr16 ld(<R16>, M(<R16>)); ld hl, (de)
// L:mv -r8 ld(M(<value>), <R8>); ld (100), a
// M:mv -r16 ld(M(<value>), <R16>); ld (100), hl
// N:mv -l ld(M(<value>), L(<R16>)); ld (100), l
// O:mv -h ld(M(<value>), H(<R16>)); ld (100), h
// P:mr16-r8 ld(M(<R16>), <R8>); ld (hl), a
// Q:mr16-r16 ld(M(<R16>), <R16>); ld (de), hl
// R:mr16-l ld(M(<R16>), L(<R16>)); ld (de), l
// S:mr16-h ld(M(<R16>), H(<R16>)); ld (de), h
// T:l -v ld(L(<R16>), <value>); ld l, 100
// U:l -r8 ld(L(<R16>), <R8>); ld l, a
// V:l -mv ld(L(<R16>), M(<value>)); ld l, (100)
// W:l -mr16 ld(L(<R16>), M(<R16>)); ld l, (de)
// X:h -v ld(H(<R16>), <value>); ld h, 100
// Y:h -r8 ld(H(<R16>), <R8>); ld h, a
// Z:h -mv ld(H(<R16>), M(<value>)); ld h, (100)
// 0:h -mr16 ld(H(<R16>), M(<R16>)); ld h, (de)
etc.
Stack Operations:
The only thing I have found a solution which I would rather a 'better' solution is the fact that a z80 only has 16bit pushes/pops and a 6502 only has only 8bit pushes/pops - meaning a bit of trickery to make them behave the same - even if simulating on real hardware. Because a Z80 pushes A and F together, restoring eg: F would also restore A even if not wanted.
We currently allow:
push(A8); and push(A16); and their pop alternatives pop(A8); and pop(A16);
Simulated Memory:
To simulate memory we have this concept of the M register modifier. On some platforms, the JS runner and a 6502 it somewhat operates like a memory pointer, or if you like... convert the content of the provided literal or register into a memory pointer.
We have some memory that we setup in arrays with the simulator currently to just cause some type of memory map to exist, it is pretty flexible.
// initialise memory
arrMemory.push({
id:'main',
enableport:1000,
disableport:1001,
startaddress:0,
endaddress:65535,
type:'memory',
subtype:'ram',
content:''
});
arrMemory.push({
id:'lowerrom',
enableport:2000,
disableport:2001,
startaddress:0,
endaddress:16384,
type:'memory',
subtype:'rom',
content:''
});
arrMemory.push({
id:'upperrom',
enableport:2002,
disableport:2003,
startaddress:49152,
endaddress:65535,
type:'memory',
subtype:'rom',
content:''
});
arrMemory.push({
id:'bank5',
enableport:3000,
disableport:3001,
startaddress:16384,
endaddress:32768,
type:'memory',
subtype:'ram',
content:''
});
arrMemory.push({
id:'bank6',
enableport:3002,
disableport:3003,
startaddress:16384,
endaddress:32768,
type:'memory',
subtype:'ram',
content:''
});
arrMemory.push({
id:'bank7',
enableport:3004,
disableport:3005,
startaddress:16384,
endaddress:32768,
type:'memory',
subtype:'ram',
content:''
});
arrMemory.push({
id:'bank8',
enableport:3006,
disableport:3007,
startaddress:16384,
endaddress:32768,
type:'memory',
subtype:'ram',
content:''
});
Simulated input / output:
I will be using my CPC output functions to provide a simulated output and input, it could be easily modified to simulate other platform output.
Also, conditional running is also implemented - so one set of code can be conditional for simulator / z80 and 6502.
Real-World uses:
I like to prototype in JS, it will be cool if i can prototype in JS and progressively rewrite the functions in assembler and still test them in the same environment. It speeds up development a lot. Ideally then hit the assemble button and get one or more working platform code-bases out.
Whether we end up with a well coded playable game - if used for a game - time will tell... watch this space.
Currently coded in normal JS, soon to be JSASM:
https://8bitology.net/poc/nzstory/
simulated memory is now working fine for 8 bit reads and writes with correct handling of main ram, mapped/unmapped rom and mapped/unmapped ram.
function main()
{
ld(B8, 5); // loads register B8 with 5
ld(M(1024), B8); // store register B8 into memory location 1024
ld(A8, M(1024)); // load register A8 with what was at location 1024
set(Z, 0); // set the Z flag to not Z (Z must be 1 to be Z)
while(test(NZ)) // loop while NZ
{
call(outA8); // call subroutine outA8
dec(A8); // decrement A8
}
}
function outA8()
{
output(1, A8); // output regsiter A8 to port 1
}
call(main); // call subroutine main
if this same code can run identically within a browser, on a CPC and on a C64 - would anyone other than me want to use it?
I have now implemented most instructions with the exception of an IN instruction, BIT operations and memory block copies.
Quote from: zhulien on 13:48, 19 November 21It works by coding your assembly language in a hybrid JS / Assembly language and you can run and debug it in your browser - no more crashing or annoying inside emulator development. The goal is to convert the virtual assembler to native assembler - which is a lot easier than a full blown compiler and we get better resulting code.
That is a pretty cool project! I could absolutely see me using this for prototyping some ideas and shortening my current development cycle. When will it be publicly available for some beta testing? :D
Also had a look at https://8bitology.net/poc/nzstory/ but how am I supposed to interact with it?
The NZ story thing is just a set of js game libraries i have coded with the intent to one day conver to z80, but I do see a lot of things not great decisions on an 8bit computer.
The jsasm cross assembler I can make available very soon as an alpha type release if you want to give feedback.
What it allows currently is to code logic in mnemonics coded as js functions and run them in the browser.
Besides the bit operators for shifting and block copies I believe something useful could be developed. The next step is of course translate those jsfunction-based mnemonics into native mnemonics which is a lot easier than when it was js to z80 (other thread). I do still want to finish the js compiler but instead of js to z80 it would be js to jsasm so it supports multiple targets.
I will post when the jsasm is deployed, yes you can self host it too if you prefer.