@@ -46,3 +46,6 @@ ioSeek: | |||
ld ix, (IO_IN_SEEK) | |||
jp (ix) | |||
ioTell: | |||
ld ix, (IO_IN_TELL) | |||
jp (ix) |
@@ -36,10 +36,19 @@ | |||
; about actual output, but only about the length of each upcode. This means | |||
; that when we parse instructions and directive that error out because of a | |||
; missing symbol, we don't error out and just write down a dummy value. | |||
.equ ZASM_FIRST_PASS RAMSTART | |||
.equ ZASM_FIRST_PASS RAMSTART | |||
; The offset where we currently are with regards to outputting opcodes | |||
.equ ZASM_PC ZASM_FIRST_PASS+1 | |||
.equ ZASM_RAMEND ZASM_PC+2 | |||
.equ ZASM_PC ZASM_FIRST_PASS+1 | |||
; whether we're in "local pass", that is, in local label scanning mode. During | |||
; this special pass, ZASM_FIRST_PASS will also be set so that the rest of the | |||
; code behaves as is we were in the first pass. | |||
.equ ZASM_LOCAL_PASS ZASM_PC+2 | |||
; I/O position (in terms of ioSeek/ioTell) of the current context. Used to | |||
; rewind to it after having parsed local labels. | |||
.equ ZASM_CTX_POS ZASM_LOCAL_PASS+1 | |||
; What ZASM_PC was when we started our context | |||
.equ ZASM_CTX_PC ZASM_CTX_POS+2 | |||
.equ ZASM_RAMEND ZASM_CTX_PC+2 | |||
; *** Code *** | |||
jp zasmMain | |||
@@ -66,7 +75,10 @@ zasmMain: | |||
ld a, l | |||
ld de, IO_OUT_GETC | |||
call JUMP_BLKSEL | |||
; Init modules | |||
xor a | |||
ld (ZASM_LOCAL_PASS), a | |||
call ioInit | |||
call symInit | |||
@@ -89,6 +101,12 @@ zasmIsFirstPass: | |||
cp 1 | |||
ret | |||
; Sets Z according to whether we're in local pass. | |||
zasmIsLocalPass: | |||
ld a, (ZASM_LOCAL_PASS) | |||
cp 1 | |||
ret | |||
; Increase (ZASM_PC) by A | |||
incOutputOffset: | |||
push de | |||
@@ -105,13 +123,21 @@ zasmParseFile: | |||
ld de, 0 | |||
ld (ZASM_PC), de | |||
.loop: | |||
inc de | |||
call parseLine | |||
ret nz ; error | |||
ld a, b ; TOK_* | |||
cp TOK_EOF | |||
ret z ; if EOF, return now with success | |||
jr z, .eof | |||
jr .loop | |||
.eof: | |||
call zasmIsLocalPass | |||
jr nz, .end ; EOF and not local pass | |||
; we're in local pass and EOF. Unwind this | |||
call _endLocalPass | |||
jr .loop | |||
.end: | |||
cp a ; ensure Z | |||
ret | |||
; Parse next token and accompanying args (when relevant) in I/O, write the | |||
; resulting opcode(s) through ioPutC and increases (ZASM_PC) by the number of | |||
@@ -178,18 +204,32 @@ _parseDirec: | |||
_parseLabel: | |||
; The string in (scratchpad) is a label with its trailing ':' removed. | |||
ld hl, scratchpad | |||
call zasmIsLocalPass | |||
jr z, .processLocalPass | |||
; Is this a local label? If yes, we don't process it in the context of | |||
; parseLine, whether it's first or second pass. Local labels are only | |||
; parsed during the Local Pass | |||
call symIsLabelLocal | |||
jr z, .success ; local? don't do anything. | |||
call zasmIsFirstPass | |||
jr z, .registerLabel ; When we encounter a label in the first | |||
; pass, we register it in the symbol | |||
; list | |||
; When we're not in the first pass, we set the context (if label is not | |||
; local) to that label. | |||
; At this point, we're in second pass, we've encountered a global label | |||
; and we'll soon continue processing our file. However, before we do | |||
; that, we should process our local labels. | |||
call _beginLocalPass | |||
jr .success | |||
.processLocalPass: | |||
call symIsLabelLocal | |||
jr z, .success ; local? don't set context | |||
call symSetContext | |||
jr z, .success | |||
; NZ? this means that (HL) couldn't be found in symbol list. Weird | |||
jr .error | |||
jr z, .registerLabel ; local label? all good, register it | |||
; normally | |||
; not a local label? Then we need to end local pass | |||
call _endLocalPass | |||
jr .success | |||
.registerLabel: | |||
ld de, (ZASM_PC) | |||
call symRegister | |||
@@ -201,3 +241,38 @@ _parseLabel: | |||
.error: | |||
call JUMP_UNSETZ | |||
ret | |||
_beginLocalPass: | |||
; remember were I/O was | |||
call ioTell | |||
ld (ZASM_CTX_POS), hl | |||
; Remember where PC was | |||
ld hl, (ZASM_PC) | |||
ld (ZASM_CTX_PC), hl | |||
; Fake first pass | |||
ld a, 1 | |||
ld (ZASM_FIRST_PASS), a | |||
; Set local pass | |||
ld (ZASM_LOCAL_PASS), a | |||
; Empty local label registry | |||
xor a | |||
ld (SYM_LOC_NAMES), a | |||
call symSelectLocalRegistry | |||
ret | |||
_endLocalPass: | |||
call symSelectGlobalRegistry | |||
; recall I/O pos | |||
ld hl, (ZASM_CTX_POS) | |||
call ioSeek | |||
; recall PC | |||
ld hl, (ZASM_CTX_PC) | |||
ld (ZASM_PC), hl | |||
; unfake first pass | |||
xor a | |||
ld (ZASM_FIRST_PASS), a | |||
; Unset local pass | |||
ld (ZASM_LOCAL_PASS), a | |||
cp a ; ensure Z | |||
ret |
@@ -162,6 +162,7 @@ parseNumberOrSymbol: | |||
ret z ; first pass? we don't care about the value, | |||
; return success. | |||
; Not a number. Try symbol | |||
call symSelect | |||
call symFind | |||
ret nz ; not found | |||
; Found! index in A, let's fetch value | |||
@@ -1,11 +1,14 @@ | |||
; Manages both constants and labels within a same namespace and registry. | |||
; | |||
; About local labels: They are treated as regular labels except they start with | |||
; a dot (example: ".foo"). Because labels are registered in order and because | |||
; constants are registered in the second pass, they end up at the end of the | |||
; symbol list and don't mix with labels. Therefore, we easily iterate through | |||
; local labels of a context by starting from that context's index and iterating | |||
; as long as symbol name start with a '.' | |||
; Local Labels | |||
; | |||
; Local labels during the "official" first pass are ignored. To register them | |||
; in the global registry during that pass would be wasteful in terms of memory. | |||
; | |||
; What we don instead is set up a separate register for them and have a "second | |||
; first pass" whenever we encounter a new context. That is, we wipe the local | |||
; registry, parse the code until the next global symbol (or EOF), then rewind | |||
; and continue second pass as usual. | |||
; *** Constants *** | |||
; Duplicate symbol in registry | |||
@@ -14,30 +17,30 @@ | |||
.equ SYM_ERR_FULLBUF 0x02 | |||
; Maximum number of symbols we can have in the registry | |||
.equ SYM_MAXCOUNT 0x100 | |||
.equ SYM_MAXCOUNT 0x100 | |||
; Size of the symbol name buffer size. This is a pool. There is no maximum name | |||
; length for a single symbol, just a maximum size for the whole pool. | |||
.equ SYM_BUFSIZE 0x1000 | |||
.equ SYM_BUFSIZE 0x1000 | |||
; Size of the names buffer for the local context registry | |||
.equ SYM_LOC_BUFSIZE 0x200 | |||
; *** Variables *** | |||
; Each symbol is mapped to a word value saved here. | |||
.equ SYM_VALUES SYM_RAMSTART | |||
.equ SYM_VALUES SYM_RAMSTART | |||
; A list of symbol names separated by null characters. When we encounter a | |||
; symbol name and want to get its value, we search the name here, retrieve the | |||
; index of the name, then go get the value at that index in SYM_VALUES. | |||
.equ SYM_NAMES SYM_VALUES+(SYM_MAXCOUNT*2) | |||
; Index of the symbol found during the last symSetContext call | |||
.equ SYM_CONTEXT_IDX SYM_NAMES+SYM_BUFSIZE | |||
.equ SYM_NAMES SYM_VALUES+(SYM_MAXCOUNT*2) | |||
; Pointer, in the SYM_NAMES buffer, of the string found during the last | |||
; symSetContext call | |||
.equ SYM_CONTEXT_PTR SYM_CONTEXT_IDX+1 | |||
; Registry for local labels. Wiped out after each context change. | |||
.equ SYM_LOC_VALUES SYM_NAMES+SYM_BUFSIZE | |||
.equ SYM_LOC_NAMES SYM_LOC_VALUES+(SYM_MAXCOUNT*2) | |||
; Pointer to the currently selected registry | |||
.equ SYM_CTX_NAMES SYM_CONTEXT_PTR+2 | |||
.equ SYM_CTX_NAMES SYM_LOC_NAMES+SYM_LOC_BUFSIZE | |||
.equ SYM_CTX_NAMESEND SYM_CTX_NAMES+2 | |||
.equ SYM_CTX_VALUES SYM_CTX_NAMESEND+2 | |||
@@ -67,15 +70,35 @@ _symNext: | |||
symInit: | |||
xor a | |||
ld (SYM_NAMES), a | |||
ld (SYM_CONTEXT_IDX), a | |||
ld hl, SYM_CONTEXT_PTR | |||
ld (SYM_CONTEXT_PTR), hl | |||
ld (SYM_LOC_NAMES), a | |||
; Continue to symSelectGlobalRegistry | |||
symSelectGlobalRegistry: | |||
push af | |||
push hl | |||
ld hl, SYM_NAMES | |||
ld (SYM_CTX_NAMES), hl | |||
ld hl, SYM_NAMES+SYM_BUFSIZE | |||
ld (SYM_CTX_NAMESEND), hl | |||
ld hl, SYM_VALUES | |||
ld (SYM_CTX_VALUES), hl | |||
pop hl | |||
pop af | |||
ret | |||
symSelectLocalRegistry: | |||
push af | |||
push hl | |||
ld hl, SYM_LOC_NAMES | |||
ld (SYM_CTX_NAMES), hl | |||
ld hl, SYM_LOC_NAMES+SYM_LOC_BUFSIZE | |||
ld (SYM_CTX_NAMESEND), hl | |||
ld hl, SYM_LOC_VALUES | |||
ld (SYM_CTX_VALUES), hl | |||
ld a, h | |||
ld a, l | |||
pop hl | |||
pop af | |||
ret | |||
; Sets Z according to whether label in (HL) is local (starts with a dot) | |||
@@ -153,6 +176,11 @@ symRegister: | |||
ld b, 0 | |||
ldir ; copy C chars from HL to DE | |||
; We need to add a second null char to indicate the end of the name | |||
; list. DE is already correctly placed. | |||
xor a | |||
ld (de), a | |||
; I'd say we're pretty good just about now. What we need to do is to | |||
; save the value in our original DE that is just on top of the stack | |||
; into the proper index in (SYM_CTX_VALUES). Our index, remember, is | |||
@@ -176,16 +204,16 @@ symRegister: | |||
pop hl | |||
ret | |||
; Select global or local registry according to label name in (HL) | |||
symSelect: | |||
call symIsLabelLocal | |||
jp z, symSelectLocalRegistry | |||
jp symSelectGlobalRegistry | |||
; Find name (HL) in (SYM_CTX_NAMES) and returns matching index in A. | |||
; If we find something, Z is set, otherwise unset. | |||
symFind: | |||
push hl | |||
call _symFind | |||
pop hl | |||
ret | |||
; Same as symFind, but leaks HL | |||
_symFind: | |||
push bc | |||
push de | |||
@@ -193,21 +221,10 @@ _symFind: | |||
call strlen | |||
ld c, a ; let's save that | |||
call symIsLabelLocal ; save Z for after the 3 next lines, which | |||
; doesn't touch flags. We need to call this now | |||
; before we lose HL. | |||
ex hl, de ; it's easier if HL is haystack and DE is | |||
; needle. | |||
ld b, 0 | |||
ld hl, (SYM_CTX_NAMES) | |||
jr nz, .loop ; not local? jump right to loop | |||
; local? then we need to adjust B and HL | |||
ld hl, (SYM_CONTEXT_PTR) | |||
ld a, (SYM_CONTEXT_IDX) | |||
ld b, a | |||
xor a | |||
sub b | |||
ld b, a | |||
.loop: | |||
ld a, c ; recall strlen | |||
call JUMP_STRNCMP | |||
@@ -218,6 +235,7 @@ _symFind: | |||
djnz .loop | |||
; exhausted djnz? no match | |||
.nomatch: | |||
out (99), a | |||
call JUMP_UNSETZ | |||
jr .end | |||
.match: | |||
@@ -228,13 +246,14 @@ _symFind: | |||
.end: | |||
pop de | |||
pop bc | |||
pop hl | |||
ret | |||
; Return value associated with symbol index A into DE | |||
symGetVal: | |||
; our index is in A. Let's fetch the proper value | |||
push hl | |||
ld hl, SYM_VALUES | |||
ld hl, (SYM_CTX_VALUES) | |||
call JUMP_ADDHL | |||
call JUMP_ADDHL ; twice because our values are words | |||
ld e, (hl) | |||
@@ -242,18 +261,3 @@ symGetVal: | |||
ld d, (hl) | |||
pop hl | |||
ret | |||
; Find symbol name (HL) in the symbol list and set SYM_CONTEXT_* accordingly. | |||
; When symFind will be called with a symbol name starting with a '.', the search | |||
; will begin at that context instead of the beginning of the register. | |||
; Sets Z if symbol is found, unsets it if not. | |||
symSetContext: | |||
push hl | |||
call _symFind | |||
jr nz, .end ; Z already unset | |||
ld (SYM_CONTEXT_IDX), a | |||
ld (SYM_CONTEXT_PTR), hl | |||
; Z already set | |||
.end: | |||
pop hl | |||
ret |
@@ -41,7 +41,7 @@ static uint8_t mem[0x10000]; | |||
static uint8_t inpt[STDIN_BUFSIZE]; | |||
static int inpt_size; | |||
static int inpt_ptr; | |||
static uint8_t received_first_seek_byte = 0; | |||
static uint8_t middle_of_seek_tell = 0; | |||
static uint8_t io_read(int unused, uint16_t addr) | |||
{ | |||
@@ -52,6 +52,17 @@ static uint8_t io_read(int unused, uint16_t addr) | |||
} else { | |||
return 0; | |||
} | |||
} else if (addr == STDIN_SEEK) { | |||
if (middle_of_seek_tell) { | |||
middle_of_seek_tell = 0; | |||
return inpt_ptr & 0xff; | |||
} else { | |||
#ifdef DEBUG | |||
fprintf(stderr, "tell %d\n", inpt_ptr); | |||
#endif | |||
middle_of_seek_tell = 1; | |||
return inpt_ptr >> 8; | |||
} | |||
} else { | |||
fprintf(stderr, "Out of bounds I/O read: %d\n", addr); | |||
return 0; | |||
@@ -67,12 +78,15 @@ static void io_write(int unused, uint16_t addr, uint8_t val) | |||
putchar(val); | |||
#endif | |||
} else if (addr == STDIN_SEEK) { | |||
if (received_first_seek_byte) { | |||
if (middle_of_seek_tell) { | |||
inpt_ptr |= val; | |||
received_first_seek_byte = 0; | |||
middle_of_seek_tell = 0; | |||
#ifdef DEBUG | |||
fprintf(stderr, "seek %d\n", inpt_ptr); | |||
#endif | |||
} else { | |||
inpt_ptr = (val << 8) & 0xff00; | |||
received_first_seek_byte = 1; | |||
middle_of_seek_tell = 1; | |||
} | |||
} else { | |||
fprintf(stderr, "Out of bounds I/O write: %d / %d\n", addr, val); | |||
@@ -51,10 +51,18 @@ emulSeek: | |||
out (STDIN_SEEK), a | |||
ret | |||
emulTell: | |||
; same principle as STDIN_SEEK | |||
in a, (STDIN_SEEK) | |||
ld h, a | |||
in a, (STDIN_SEEK) | |||
ld l, a | |||
ret | |||
#include "core.asm" | |||
.equ BLOCKDEV_RAMSTART RAMSTART | |||
.equ BLOCKDEV_COUNT 2 | |||
#include "blockdev.asm" | |||
; List of devices | |||
.dw emulGetC, 0, emulSeek, 0 | |||
.dw emulGetC, 0, emulSeek, emulTell | |||
.dw 0, emulPutC, 0, 0 |