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.
This commit is contained in:
Virgil Dupras 2019-11-18 13:40:23 -05:00
parent 019d05f64c
commit 1cea6e71e0
10 changed files with 302 additions and 34 deletions

14
CODE.md
View File

@ -32,6 +32,20 @@ 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.
## 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,10 @@
.inc "core.asm"
.inc "lib/util.asm"
.inc "lib/ari.asm"
.inc "lib/parse.asm"
.inc "lib/fmt.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 parseDecimal
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

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

@ -26,3 +26,29 @@ 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

View File

@ -98,32 +98,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.

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: