1
0
mirror of https://github.com/hsoft/collapseos.git synced 2024-11-26 03:18:05 +11:00

Compare commits

...

3 Commits

Author SHA1 Message Date
Virgil Dupras
f5b04fc02f basic: add expression support to print
Again, same thing as in zasm.
2019-11-18 15:52:44 -05:00
Virgil Dupras
0bd58fd178 basic: parse hex, binary and char literals
Same thing as in zasm.
2019-11-18 15:22:09 -05:00
Virgil Dupras
1cea6e71e0 basic: add a print cmd
It can only print a decimal literal. But still, that's a big step because
I hadn't implemented decimal formatting yet.
2019-11-18 13:40:23 -05:00
17 changed files with 532 additions and 243 deletions

19
CODE.md
View File

@ -32,6 +32,25 @@ Thus, code that glue parts together could look like:
MOD2_RAMSTART .equ MOD1_RAMEND
#include "mod2.asm"
## Register protection
As a general rule, all routines systematically protect registers they use,
including input parameters. This allows us to stop worrying, each time we call
a routine, whether our registers are all messed up.
Some routines stray from that rule, but the fact that they destroy a particular
register is documented. An undocumented register change is considered a bug.
Clean up after yourself, you nasty routine!
Another exception to this rule are "top-level" routines, that is, routines that
aren't designed to be called from other parts of Collapse OS. Those are
generally routines close to an application's main loop.
It is important to note, however, that shadow registers aren't preserved.
Therefore, shadow registers should only be used in code that doesn't call
routines or that call a routine that explicitly states that it preserves
shadow registers.
## Stack management
Keeping the stack "balanced" is a big challenge when writing assembler code.

View File

@ -1,6 +1,8 @@
This file describe tricks and conventions that are used throughout the code and
might need explanation.
*** Quickies
or a: Equivalent to "cp 0", but results in a shorter opcode.
xor a: sets A to 0 more efficiently than ld a, 0
@ -8,12 +10,16 @@ xor a: sets A to 0 more efficiently than ld a, 0
and 0xbf: Given a letter in the a-z range, changes it to its uppercase value
if it's already uppercased, then it stays that way.
*** Z flag for results
Z if almost always used as a success indicator for routines. Set for success,
Reset for failure. "xor a" (destroys A) and "cp a" (preserves A) are used to
ensure Z is set. To ensure that it is reset, it's a bit more complicated and
"unsetZ" routine exists for that, although that in certain circumstances,
"inc a \ dec a" or "or a" can work.
*** Little endian
z80 is little endian in its 16-bit loading operations. For example, "ld hl, (0)"
will load the contents of memory address 0 in L and memory address 1 in H. This
little-endianess is followed by Collapse OS in most situations. When it's not,
@ -25,3 +31,21 @@ are stored as "big endian pair of little endian 16-bit numbers". For example,
if "ld dehl, (0)" existed and if the first 4 bytes of memory were 0x01, 0x02,
0x03 and 0x04, then DE (being the "high" word) would be 0x0201 and HL would be
0x0403.
*** DAA
When it comes to dealing with decimals, the DAA instruction, which look a bit
obscur, can be very useful. It transforms the result of a previous arithmetic
operation involving two BCD (binary coded decimal, one digit in high nibble,
the other digit in low nibble. For example, 0x99 represents 99) into a valid
BCD. For example, 0x12+0x19=0x2b, but after calling DAA, it will be 0x31.
To clear misunderstanding: this does **not** transform an arbitrary value into
BCD. For example, "ld a, 0xff \ daa" isn't going to magically give you a binary
coded 255 (how could it?). This is designed to be ran after an arithmetic
operation.
A common trick to transform an arbitrary number to BCD is to loop 8 times over
your bitstream, SLA your bits out of your binary value and then run
"adc a, a \ daa" over it (with provisions for carries if you expect numbers
over 99).

View File

@ -11,7 +11,12 @@
.inc "core.asm"
.inc "lib/util.asm"
.inc "lib/ari.asm"
.inc "lib/parse.asm"
.inc "lib/fmt.asm"
.equ EXPR_PARSE parseLiteral
.inc "lib/expr.asm"
.inc "basic/tok.asm"
.equ BAS_RAMSTART USER_RAMSTART
.inc "basic/main.asm"
USER_RAMSTART:

View File

@ -1,10 +1,13 @@
; *** Constants ***
.equ BAS_SCRATCHPAD_SIZE 0x20
; *** Variables ***
; Value of `SP` when basic was first invoked. This is where SP is going back to
; on restarts.
.equ BAS_INITSP BAS_RAMSTART
; **Pointer** to current line number
.equ BAS_PCURLN @+2
.equ BAS_RAMEND @+2
.equ BAS_SCRATCHPAD @+2
.equ BAS_RAMEND @+BAS_SCRATCHPAD_SIZE
; *** Code ***
basStart:
@ -39,24 +42,37 @@ basPrompt:
.db "> ", 0
basDirect:
; First, get cmd length
call fnWSIdx
cp 7
jr nc, .unknown ; Too long, can't possibly fit anything.
; A contains whitespace IDX, save it in B
ld b, a
ex de, hl
ld hl, basCmds1
ld hl, basCmds1+2
.loop:
ld a, 4
ld a, b ; whitespace IDX
call strncmp
jr z, .found
ld a, 6
ld a, 8
call addHL
ld a, (hl)
cp 0xff
jr nz, .loop
.unknown:
ld hl, .sUnknown
jr basPrintLn
.found:
inc hl \ inc hl \ inc hl \ inc hl
dec hl \ dec hl
call intoHL
jp (hl)
push hl \ pop ix
; Bring back command string from DE to HL
ex de, hl
ld a, b ; cmd's length
call addHL
call rdWS
jp (ix)
.sUnknown:
.db "Unknown command", 0
@ -66,7 +82,17 @@ basPrintLn:
call printstr
jp printcrlf
basERR:
ld hl, .sErr
jr basPrintLn
.sErr:
.db "ERR", 0
; *** Commands ***
; A command receives its argument through (HL), which is already placed to
; either:
; 1 - the end of the string if the command has no arg.
; 2 - the beginning of the arg, with whitespace properly skipped.
basBYE:
ld hl, .sBye
call basPrintLn
@ -78,10 +104,20 @@ basBYE:
.sBye:
.db "Goodbye!", 0
basPRINT:
call parseExpr
jp nz, basERR
push ix \ pop de
ld hl, BAS_SCRATCHPAD
call fmtDecimal
jp basPrintLn
; direct only
basCmds1:
.db "bye", 0
.dw basBYE
.db "bye", 0, 0, 0
; statements
basCmds2:
.db 0xff ; end of table
.dw basPRINT
.db "print", 0
.db 0xff, 0xff, 0xff ; end of table

48
apps/basic/tok.asm Normal file
View File

@ -0,0 +1,48 @@
; Expect at least one whitespace (0x20, 0x09) at (HL), and then advance HL
; until a non-whitespace character is met.
; HL is advanced to the first non-whitespace char.
; Sets Z on success, unset on failure.
; Failure is either not having a first whitespace or reaching the end of the
; string.
; Sets Z if we found a non-whitespace char, unset if we found the end of string.
rdWS:
ld a, (hl)
call isSep
ret nz ; failure
.loop:
inc hl
ld a, (hl)
call isSep
jr z, .loop
or a ; cp 0
jp z, .fail
cp a ; ensure Z
ret
.fail:
; A is zero at this point
inc a ; unset Z
ret
; Find the first whitespace in (HL) and returns its index in A
; Sets Z if whitespace is found, unset if end of string was found.
; In the case where no whitespace was found, A returns the length of the string.
fnWSIdx:
push hl
push bc
ld b, 0
.loop:
ld a, (hl)
call isSep
jr z, .found
or a
jr z, .eos
inc hl
inc b
jr .loop
.eos:
inc a ; unset Z
.found: ; Z already set from isSep
ld a, b
pop bc
pop hl
ret

27
apps/lib/ari.asm Normal file
View File

@ -0,0 +1,27 @@
; Borrowed from Tasty Basic by Dimitri Theulings (GPL).
; Divide HL by DE, placing the result in BC and the remainder in HL.
divide:
push hl ; --> lvl 1
ld l, h ; divide h by de
ld h, 0
call .dv1
ld b, c ; save result in b
ld a, l ; (remainder + l) / de
pop hl ; <-- lvl 1
ld h, a
.dv1:
ld c, 0xff ; result in c
.dv2:
inc c ; dumb routine
call .subde ; divide using subtract and count
jr nc, .dv2
add hl, de
ret
.subde:
ld a, l
sub e ; subtract de from hl
ld l, a
ld a, h
sbc a, d
ld h, a
ret

View File

@ -1,3 +1,15 @@
; *** Requirements ***
; findchar
; multDEBC
;
; *** Defines ***
;
; EXPR_PARSE: routine to call to parse literals or symbols that are part of
; the expression. Routine's signature:
; String in (HL), returns its parsed value to IX. Z for success.
;
; *** Code ***
;
; Parse expression in string at (HL) and returns the result in IX.
; We expect (HL) to be disposable: we mutate it to avoid having to make a copy.
; Sets Z on success, unset on error.
@ -19,7 +31,7 @@ _parseExpr:
ld a, '*'
call _findAndSplit
jp z, _applyMult
jp parseNumberOrSymbol
jp EXPR_PARSE
; Given a string in (HL) and a separator char in A, return a splitted string,
; that is, the same (HL) string but with the found A char replaced by a null

46
apps/lib/fmt.asm Normal file
View File

@ -0,0 +1,46 @@
; Format the number in DE into the string at (HL) in a decimal form.
; Null-terminated. DE is considered an unsigned number.
fmtDecimal:
push ix
push hl
push de
push af
push hl \ pop ix
ex de, hl ; orig number now in HL
ld e, 0
.loop1:
call .div10
push hl ; push remainder. --> lvl E
inc e
ld a, b ; result 0?
or c
push bc \ pop hl
jr nz, .loop1 ; not zero, continue
; We now have C digits to print in the stack.
; Spit them!
push ix \ pop hl ; restore orig HL.
ld b, e
.loop2:
pop de ; <-- lvl E
ld a, '0'
add a, e
ld (hl), a
inc hl
djnz .loop2
; null terminate
xor a
ld (hl), a
pop af
pop de
pop hl
pop ix
ret
.div10:
push de
ld de, 0x000a
call divide
pop de
ret

View File

@ -136,3 +136,168 @@ parseDecimal:
.error:
pop hl
jp unsetZ
; Parse string at (HL) as a hexadecimal value and return value in IX under the
; same conditions as parseLiteral.
parseHexadecimal:
call hasHexPrefix
ret nz
push hl
push de
ld d, 0
inc hl ; get rid of "0x"
inc hl
call strlen
cp 3
jr c, .single
cp 4
jr c, .doubleShort ; 0x123
cp 5
jr c, .double ; 0x1234
; too long, error
jr .error
.double:
call parseHexPair
jr c, .error
inc hl ; now HL is on first char of next pair
ld d, a
jr .single
.doubleShort:
ld a, (hl)
call parseHex
jr c, .error
inc hl ; now HL is on first char of next pair
ld d, a
.single:
call parseHexPair
jr c, .error
ld e, a
cp a ; ensure Z
jr .end
.error:
call unsetZ
.end:
push de \ pop ix
pop de
pop hl
ret
; Sets Z if (HL) has a '0x' prefix.
hasHexPrefix:
ld a, (hl)
cp '0'
ret nz
push hl
inc hl
ld a, (hl)
cp 'x'
pop hl
ret
; Parse string at (HL) as a binary value (0b010101) and return value in IX.
; High IX byte is always clear.
; Sets Z on success.
parseBinaryLiteral:
call hasBinPrefix
ret nz
push bc
push hl
push de
ld d, 0
inc hl ; get rid of "0b"
inc hl
call strlen
or a
jr z, .error ; empty, error
cp 9
jr nc, .error ; >= 9, too long
; We have a string of 8 or less chars. What we'll do is that for each
; char, we rotate left and set the LSB according to whether we have '0'
; or '1'. Error out on anything else. C is our stored result.
ld b, a ; we loop for "strlen" times
ld c, 0 ; our stored result
.loop:
rlc c
ld a, (hl)
inc hl
cp '0'
jr z, .nobit ; no bit to set
cp '1'
jr nz, .error ; not 0 or 1
; We have a bit to set
inc c
.nobit:
djnz .loop
ld e, c
cp a ; ensure Z
jr .end
.error:
call unsetZ
.end:
push de \ pop ix
pop de
pop hl
pop bc
ret
; Sets Z if (HL) has a '0b' prefix.
hasBinPrefix:
ld a, (hl)
cp '0'
ret nz
push hl
inc hl
ld a, (hl)
cp 'b'
pop hl
ret
; Parse string at (HL) and, if it is a char literal, sets Z and return
; corresponding value in IX. High IX byte is always clear.
;
; A valid char literal starts with ', ends with ' and has one character in the
; middle. No escape sequence are accepted, but ''' will return the apostrophe
; character.
parseCharLiteral:
ld a, 0x27 ; apostrophe (') char
cp (hl)
ret nz
push hl
push de
inc hl
inc hl
cp (hl)
jr nz, .end ; not ending with an apostrophe
inc hl
ld a, (hl)
or a ; cp 0
jr nz, .end ; string has to end there
; Valid char, good
ld d, a ; A is zero, take advantage of that
dec hl
dec hl
ld a, (hl)
ld e, a
cp a ; ensure Z
.end:
push de \ pop ix
pop de
pop hl
ret
; Parses the string at (HL) and returns the 16-bit value in IX. The string
; can be a decimal literal (1234), a hexadecimal literal (0x1234) or a char
; literal ('X').
;
; As soon as the number doesn't fit 16-bit any more, parsing stops and the
; number is invalid. If the number is valid, Z is set, otherwise, unset.
parseLiteral:
call parseCharLiteral
ret z
call parseHexadecimal
ret z
call parseBinaryLiteral
ret z
jp parseDecimal

View File

@ -26,3 +26,67 @@ strcpy:
pop hl
ret
; Compares strings pointed to by HL and DE until one of them hits its null char.
; If equal, Z is set. If not equal, Z is reset.
strcmp:
push hl
push de
.loop:
ld a, (de)
cp (hl)
jr nz, .end ; not equal? break early. NZ is carried out
; to the called
or a ; If our chars are null, stop the cmp
jr z, .end ; The positive result will be carried to the
; caller
inc hl
inc de
jr .loop
.end:
pop de
pop hl
; Because we don't call anything else than CP that modify the Z flag,
; our Z value will be that of the last cp (reset if we broke the loop
; early, set otherwise)
ret
; Returns length of string at (HL) in A.
; Doesn't include null termination.
strlen:
push bc
push hl
ld bc, 0
xor a ; look for null char
.loop:
cpi
jp z, .found
jr .loop
.found:
; How many char do we have? the (NEG BC)-1, which started at 0 and
; decreased at each CPI call. In this routine, we stay in the 8-bit
; realm, so C only.
ld a, c
neg
dec a
pop hl
pop bc
ret
; DE * BC -> DE (high) and HL (low)
multDEBC:
ld hl, 0
ld a, 0x10
.loop:
add hl, hl
rl e
rl d
jr nc, .noinc
add hl, bc
jr nc, .noinc
inc de
.noinc:
dec a
jr nz, .loop
ret

View File

@ -31,8 +31,6 @@
; _blkSeek
; _blkTell
; printstr
; FS_HANDLE_SIZE
; BLOCKDEV_SIZE
.inc "user.h"
@ -80,7 +78,8 @@ jp zasmMain
.equ DIREC_RAMSTART INS_RAMEND
.inc "zasm/directive.asm"
.inc "zasm/parse.asm"
.inc "zasm/expr.asm"
.equ EXPR_PARSE parseNumberOrSymbol
.inc "lib/expr.asm"
.equ SYM_RAMSTART DIREC_RAMEND
.inc "zasm/symbol.asm"
.equ ZASM_RAMSTART SYM_RAMEND

View File

@ -1,167 +1,3 @@
; Parse string at (HL) as a hexadecimal value and return value in IX under the
; same conditions as parseLiteral.
parseHexadecimal:
call hasHexPrefix
ret nz
push hl
push de
ld d, 0
inc hl ; get rid of "0x"
inc hl
call strlen
cp 3
jr c, .single
cp 4
jr c, .doubleShort ; 0x123
cp 5
jr c, .double ; 0x1234
; too long, error
jr .error
.double:
call parseHexPair
jr c, .error
inc hl ; now HL is on first char of next pair
ld d, a
jr .single
.doubleShort:
ld a, (hl)
call parseHex
jr c, .error
inc hl ; now HL is on first char of next pair
ld d, a
.single:
call parseHexPair
jr c, .error
ld e, a
cp a ; ensure Z
jr .end
.error:
call unsetZ
.end:
push de \ pop ix
pop de
pop hl
ret
; Sets Z if (HL) has a '0x' prefix.
hasHexPrefix:
ld a, (hl)
cp '0'
ret nz
push hl
inc hl
ld a, (hl)
cp 'x'
pop hl
ret
; Parse string at (HL) as a binary value (0b010101) and return value in IX.
; High IX byte is always clear.
; Sets Z on success.
parseBinaryLiteral:
call hasBinPrefix
ret nz
push bc
push hl
push de
ld d, 0
inc hl ; get rid of "0b"
inc hl
call strlen
or a
jr z, .error ; empty, error
cp 9
jr nc, .error ; >= 9, too long
; We have a string of 8 or less chars. What we'll do is that for each
; char, we rotate left and set the LSB according to whether we have '0'
; or '1'. Error out on anything else. C is our stored result.
ld b, a ; we loop for "strlen" times
ld c, 0 ; our stored result
.loop:
rlc c
ld a, (hl)
inc hl
cp '0'
jr z, .nobit ; no bit to set
cp '1'
jr nz, .error ; not 0 or 1
; We have a bit to set
inc c
.nobit:
djnz .loop
ld e, c
cp a ; ensure Z
jr .end
.error:
call unsetZ
.end:
push de \ pop ix
pop de
pop hl
pop bc
ret
; Sets Z if (HL) has a '0b' prefix.
hasBinPrefix:
ld a, (hl)
cp '0'
ret nz
push hl
inc hl
ld a, (hl)
cp 'b'
pop hl
ret
; Parse string at (HL) and, if it is a char literal, sets Z and return
; corresponding value in IX. High IX byte is always clear.
;
; A valid char literal starts with ', ends with ' and has one character in the
; middle. No escape sequence are accepted, but ''' will return the apostrophe
; character.
parseCharLiteral:
ld a, 0x27 ; apostrophe (') char
cp (hl)
ret nz
push hl
push de
inc hl
inc hl
cp (hl)
jr nz, .end ; not ending with an apostrophe
inc hl
ld a, (hl)
or a ; cp 0
jr nz, .end ; string has to end there
; Valid char, good
ld d, a ; A is zero, take advantage of that
dec hl
dec hl
ld a, (hl)
ld e, a
cp a ; ensure Z
.end:
push de \ pop ix
pop de
pop hl
ret
; Parses the string at (HL) and returns the 16-bit value in IX. The string
; can be a decimal literal (1234), a hexadecimal literal (0x1234) or a char
; literal ('X').
;
; As soon as the number doesn't fit 16-bit any more, parsing stops and the
; number is invalid. If the number is valid, Z is set, otherwise, unset.
parseLiteral:
call parseCharLiteral
ret z
call parseHexadecimal
ret z
call parseBinaryLiteral
ret z
jp parseDecimal
; Parse string in (HL) and return its numerical value whether its a number
; literal or a symbol. Returns value in IX.
; Sets Z if number or symbol is valid, unset otherwise.

View File

@ -30,28 +30,6 @@ toggleZ:
cp a
ret
; Returns length of string at (HL) in A.
; Doesn't include null termination.
strlen:
push bc
push hl
ld bc, 0
ld a, 0 ; look for null char
.loop:
cpi
jp z, .found
jr .loop
.found:
; How many char do we have? the (NEG BC)-1, which started at 0 and
; decreased at each CPI call. In this routine, we stay in the 8-bit
; realm, so C only.
ld a, c
neg
dec a
pop hl
pop bc
ret
; Sets Z if string at (HL) is one character long
strIs1L:
xor a
@ -98,32 +76,6 @@ strncmpI:
; early, set otherwise)
ret
; Compares strings pointed to by HL and DE until one of them hits its null char.
; If equal, Z is set. If not equal, Z is reset.
strcmp:
push hl
push de
.loop:
ld a, (de)
cp (hl)
jr nz, .end ; not equal? break early. NZ is carried out
; to the called
cp 0 ; If our chars are null, stop the cmp
jr z, .end ; The positive result will be carried to the
; caller
inc hl
inc de
jr .loop
.end:
pop de
pop hl
; Because we don't call anything else than CP that modify the Z flag,
; our Z value will be that of the last cp (reset if we broke the loop
; early, set otherwise)
ret
; If string at (HL) starts with ( and ends with ), "enter" into the parens
; (advance HL and put a null char at the end of the string) and set Z.
; Otherwise, do nothing and reset Z.
@ -219,19 +171,3 @@ findStringInList:
ret
; DE * BC -> DE (high) and HL (low)
multDEBC:
ld hl, 0
ld a, 0x10
.loop:
add hl, hl
rl e
rl d
jr nc, .noinc
add hl, bc
jr nc, .noinc
inc de
.noinc:
dec a
jr nz, .loop
ret

View File

@ -18,7 +18,8 @@ jp test
.inc "zasm/parse.asm"
.equ SYM_RAMSTART DIREC_LASTVAL+2
.inc "zasm/symbol.asm"
.inc "zasm/expr.asm"
.equ EXPR_PARSE parseNumberOrSymbol
.inc "lib/expr.asm"
; Pretend that we aren't in first pass
zasmIsFirstPass:

View File

@ -0,0 +1,70 @@
jp test
.inc "core.asm"
.inc "lib/util.asm"
.inc "lib/ari.asm"
.inc "lib/fmt.asm"
testNum: .db 1
test:
ld sp, 0xffff
call testFmtDecimal
; success
xor a
halt
testFmtDecimal:
ld ix, .t1
call .test
ld ix, .t2
call .test
ld ix, .t3
call .test
ld ix, .t4
call .test
ld ix, .t5
call .test
ret
.test:
ld e, (ix)
ld d, (ix+1)
ld hl, sandbox
call fmtDecimal
ld hl, sandbox
push ix \ pop de
inc de \ inc de
call strcmp
jp nz, fail
jp nexttest
.t1:
.dw 1234
.db "1234", 0
.t2:
.dw 9999
.db "9999", 0
.t3:
.dw 0
.db "0", 0
.t4:
.dw 0x7fff
.db "32767", 0
.t5:
.dw 0xffff
.db "65535", 0
nexttest:
ld a, (testNum)
inc a
ld (testNum), a
ret
fail:
ld a, (testNum)
halt
; used as RAM
sandbox:

View File

@ -2,6 +2,7 @@ jp test
.inc "core.asm"
.inc "str.asm"
.inc "lib/util.asm"
.inc "zasm/util.asm"
testNum: .db 1

View File

@ -7,8 +7,8 @@ jp runTests
.inc "lib/util.asm"
.inc "zasm/util.asm"
.inc "lib/parse.asm"
.inc "zasm/parse.asm"
.inc "zasm/expr.asm"
.equ EXPR_PARSE parseLiteral
.inc "lib/expr.asm"
.equ INS_RAMSTART RAMSTART
.inc "zasm/instr.asm"