https://github.com/skx/z80-cpm-scripting-interpreter
A trivial I/O language, with repl, written in z80 assembler to run under CP/M.
https://github.com/skx/z80-cpm-scripting-interpreter
cpm io repl retro scripting scripting-language z80
Last synced: 3 months ago
JSON representation
A trivial I/O language, with repl, written in z80 assembler to run under CP/M.
- Host: GitHub
- URL: https://github.com/skx/z80-cpm-scripting-interpreter
- Owner: skx
- Created: 2023-06-22T19:46:59.000Z (almost 3 years ago)
- Default Branch: master
- Last Pushed: 2023-10-17T17:39:30.000Z (over 2 years ago)
- Last Synced: 2025-04-04T05:27:33.999Z (about 1 year ago)
- Topics: cpm, io, repl, retro, scripting, scripting-language, z80
- Language: Makefile
- Homepage:
- Size: 81.1 KB
- Stars: 1
- Watchers: 2
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# scripting
This is a trivial REPL-based "scripting thing", designed to run on Z80-based CP/M systems.
The REPL allows arbitrary I/O to ports, and RAM, along with string output and looping-support, but there is little else - for usefulness the currently-selected I/O port is displayed in the REPL prompt.
All-told the interpreter, when compiled as REPL, requires approximately 800 bytes.
## Overview
This repository contains a simple REPL-based interpreter which will run upon a CP/M host. Although there is a FORTH-like flavour to the idea of using simple tokens to define actions this is actually not a stack-based language at all.
There are three registers which are used, internally:
* Any time a number is encountered it is moved into a scratch-area.
* This is notionally known as the accumulator register.
* It is possible to move the accumulator value into the IO-register, U.
* This register contains the port-number that any I/O read, or write, operation is applied to.
* Not a great mnemonic, but close to both I&O on the keyboard.
* It is possible to move the accumulator value into the memory-register, M.
* This register specifies the RAM address of any memory read/write operations.
* Finally you may move the contents of the accumulator into the K-register, which is used for operating loops.
TLDR:
* Numbers go to A-Register.
* Port I/O is controlled by the U-register.
* RAM read/write is controlled by the M-register.
* Loops are controlled by the K-register.
### Instructions
The following instructions are available:
* `[0-9]`
* Build up a number, which is stored in the accumulator / A-register.
* `[ \t\n]`
* Ignored.
* `{..}`
* Looping construct, see below for details.
* `u`
* Set the I/O-port to be the value of the accumulator.
* `U`
* Set the accumulator to be the value of the I/O-port.
* `_`
* Print the string wrapped by by "_" characters.
* `c`
* Clear the number in the accumulator. (i.e. Set to zero).
* `g`
* Perform a CALL instruction to the currently selected RAM address in the M-register.
* `h`
* HALT for the number of times specified in the accumulator.
* This is used for running delay operations.
* `i`
* Read a byte of input, from the currently selected I/O port (U-register), and place it in the accumulator.
* `k`
* Copy the contents of the accumulator (lower half) into the K-register which is used for loops.
* `K`
* Copy the contents of the K-register to the accumulator.
* `m`
* Write the contents of the accumulator to the M-register, which is used to specify the address to (r)ead and (w)rite from.
* `M`
* Write the contents of the M-register to the accumulator.
* `n`
* Write a newline character.
* `o`
* Write the contents of the accumulator to the currently selected I/O port (held in the U-register).
* `p`
* Print the value of the accumulator, as a four-digit hex number.
* `P`
* Print the value of the lower half of the accumulator, as a two-digit hex number.
* `r`
* Read the contents of the currently selected RAM address, held in the M-register), and save in the accumulator.
* Then increment the RAM address held in the M-register (so that repeats will read from incrementing addresses).
* `q`
* Quit, if we're in REPL-mode.
* `w`
* Write the contents of the accumulator (lower byte only) to the currently selected RAM address held in the M-register.
* Then increment the RAM address held in the M-register (so that repeats will write to incrementing addresses).
* `x`
* Print the character whos ASCII code is stored in the accumulator.
## Looping
The special K-register can be used to control how many times a loop will be carried out.
Loops consist of code between `{` and `}` pairs. For example the following program will show a countdown:
```
[00]>10k{Kp}
0009
0008
0007
0006
0005
0004
0003
0002
0001
0000
```
First of all the value 10 is loaded into the accumulator, then copied into the K-register. The body of the loop is then:
```
Kp
```
K copies the contents of the loop-register back to the accumulator, which is then printed by the `p` command.
Another example would be to dump the first 16 bytes of RAM:
```
> 0m 16k{rP _ _}
C3 03 EA 00 00 C3 06 DC 00 00 00 00 00 00 00 00
```
* `0m`: Stores the address zero in the M-register
* `16k`: Sets the K-register, our loop counter, to 16.
* `{`: loop-start
* `r`: Read a byte into the accumulator from the address in the M-register, incrementing that register in the process.
* `P`: Print the contents of the accumulator as a 2-digit HEX number.
* `_ _`: Output a space, to split the output nicely.
* `}`: loop-end
### Sample "Programs"
Also note that I broke up the "programs" with whitespace to aid readability. This still works, spaces, TABs, and newlines are skipped over by the interpreter.
Show a greeting:
```
_Hello, world!_
```
Store the value 42 in the accumulator, and print it as a four-digit hex number:
```
42p
```
To print the value as a byte, not a word use `P` rather than `p`:
```
42P
```
Store the value "201" (opcode for RET) at address 20000, and CALL it, this will execute RET which will return to our REPL:
```
20000m 201w 20000mg
```
Jump to address 0x0000, which will exit back to the CP/M prompt:
```
0mg
```
Store the value 42 in the accumulator, then print that as if it were the ASCII code of a character (output "`*`"):
```
42x
```
Configure the I/O port to be port 0x01, read a byte from it to the accumulator, then print that value:
```
1u i p
```
Write the byte 32, then the byte 77, to the I/O port 3.
```
3u 32o 77o
```
To be more explicit that last example could have been written as:
* `3u32o77o`
* `3` - Write 3 to the accumulator.
* `u` - Set the I/O port to be used for (i)nput and (o)utput to be the value in the accumulator, i.e. 3.
* `32`- Write 32 to the accumulator.
* `o` - Output the byte in the accumulator (32) to the currently selected I/O port (3)
* `77`- Write 77 to the accumulator.
* `o` - Output the byte in the accumulator (77) to the currently selected I/O port (3)
## Porting
We use the CP/M BIOS calls for simplicity, if you wished to port this code to a single-board Z80-based system, without CP/M, that would not be hard.
All the system-integration is contained within the file [bios.z80](bios.z80) so you could replace the functions appropriately:
* Add your UART initialization to `bios_init`.
* Update the code to read/write to the serial-console, or whatever, in the other I/O functions.
## Inspiration
https://blog.steve.fi/simple_toy_languages.html