Keeping things simple - Tiny Languages

One thing that resonated with me about FOCAL is its use of single uppercase characters as its unique commands, and variable names are formed from 2 or more characters, but only the first two are significant.

When you are memory and processing power limited, this makes absolute sense as it makes the line parsing so much easier, efficient and saves precious words of memory.

The early Forths used to do a dictionary search based on the first 3 characters and the length of the word. This adds flexibility but at the expense of additional complexity.

What about a minimalist language using single, printable ascii characters as the commands, variables and operations? The ascii value of the character provides a direct index into a look up table, and from this table a Jump address is loaded into the Program Counter.

Based on this concept, the inner interpreter of an extensible interactive language could be reduced to a jump table and a couple of hundred bytes of primitive instructions and support routines.

The concept is not new, single keypress commands have been used for decades in machine code monitors, but can this technique be extended to create a useful language, from which useful applications may be written?

Considering the example of the 6502. Could it provide sufficient functionality to allow hardware “bring up” and debug purposes to be performed more easily than a hex-monitor - such as WozMon?

So you use WozMon to type in a couple of hundred of bytes of hex codes and then you have a higher level extensible language, making code development somewhat easier.

One such example of this was the VTL-2 (Very Tiny Language) which reportedly fitted into 768 bytes of the three extension ROMs in the early 6800 system from Altair. VTL-2 has subsequently been ported to 6502 - and a few years ago was discussed here on the 6502.org forum.

http://forum.6502.org/viewtopic.php?f=2&t=2612

Porting languages between processors is something that was frequently done in the 1970s, and it can either be done using brute force, transcoding each fuunction in turn. Alternatively it can be done by way of a virtual machine - in a similar manner to Wozniak’s Sweet-16 or Tiny BASICs Interpretive Language (IL)

A more recent example is Marcel van Kervink’s Gigatron TTL Computer - where a 16-bit von Neuman virtual machine has been built on top of a Harvard 8-bit hardware architecture.

It could be argued that for most computing applications, 16-bit integer arithmetic is about the minimum pre-requisite. Even VTL-2 was built around 16-bit requirements.

The well known “From Nand to Teris” course implemented a 16-bit stack based VM on top of an insanely simple hardware architecture. This layer of abstraction turns the process of programming the machine from an impossibly frustrating machine code to something a little more human readable.

So we need a simple 16-bit VM, with a reduced instruction set and the means to enter code directly from the keyboard on a self hosted system. Can we do this in 768 bytes, unlikely, but I have done something similar in 900 bytes on an MSP430.

So my challenge for the rest of this week is to squeeze a useful extensible programming language with editor and assembler into 1024 bytes.

3 Likes

“Extensible” raises the bar nicely. It is possible to be too simple, as was the case for the Pilot educational language (which used single letter commands as you suggested).

For a client I once fit a simple self hosted programming language into a PIC16F84 and its 1024 instructions. The bulk of the code was for a software serial port (they already had this chip in stock and didn’t want to switch to a PIC with a hardware UART). The language had just a few commands: “wait”, “check”, “output” and “halt”. The first started a new thread (up to 8) and would execute when the indicated input had the indicated value.

You could change the program (stored in the EEPROM, so up to 64 instructions long) in hex interactively using a terminal on the serial port. With a few more ROM bytes I could have squeezed in a text representation, but since that wasn’t possible I created some macros that allowed the PIC assembler to convert the text source to the hex to be typed in.

2 Likes

Yes - extensible is the key.

The nucleus can be very small but user functions can be defined in a similar way to Forth, with colon definitions.

When I first started developing this language, it started as a series of single character commands with one numerical parameter, sent over a serial connection to exercise some hardware during the debug phase.

I very quickly learned that 2 or more numerical parameters would be far more useful - so it quickly evolved into a stack based machine that would interpret various commands and parameters sent over a serial link.

It’s nearest recognisable cousin is probably G-code, as I was using it to control X-Y motion systems. But that’s G code sent with parameters first, operator second.

It started with simple move commands for a motion platform such as 100 U, which meant move the platform Up 100mm. Other commands allowed me to set the speed of travel - and so things evolved from there.

Each of the commands was an upper case charater chosen to have a strong mnemonic value. Eveyone can remember U for Up and D for down.

About the same time, (May 2013) I went to a lecture one evening in London, where I learnt about a simple interpreter written in C++ to exercise the Arduino hardware. It only had 13 commands, no math, no logic - but it was enough for me to see the flexibility, and the extensibility.
The interpreter was written by Ward Cunningham and he called it txtzyme - a concatenation of text and enzyme. He described it as a catalyst to enable creativity. The original txtzyme is here:

By 2013, Arduino had started to branch out into other processors away from the original ATmega range. I came across Energia - that was almost a carbon copy of the Arduino IDE, but targetted the TI MSP430 series. I quickly ported Txtzyme to an MSP430 dev-board using Energia, and it worked straight out of the can. I had learned my first real lesson in code portability.

Txtzyme was fun, but limited with only 13 ascii commands, so I quickly scanned a table of ascii, and realised that there are 95 printable characters ( and backspace). Here was the route to extensibility.

I quickly added the math operators:
ADD +
SUB -
MUL *
DIV /

LIT # - a 16-bit literal

and the logical operators:
AND &
OR |
XOR ^
NEG ~

And some comparisons

GT >
LT <
EQ =

Some stack operations:

DUP "
DROP ’
SWAP $
OVER %

Memory access - borrowed from Forth:

Fetch @
Store !

And some looping, program flow control and misc operations - all involving various parentheses
{ }
( )
_ _

Comma is used as a separator in a numerical array (1,2,3,4,5)

And Space is used as a delimiter to separate numerical parameters implying that each is pushed on the stack when a space is encountered

Input and Output was borrowed from Forth:

? Key (serial input)
. Print (serial output)

And finally the colon definition - again borrowed from Forth
: Call
; Ret

And before I knew it - I had used 29 of the printable characters. Sure there’s tick ` and £ and \ but these can be defined later.

The numerical characters are used to enter numerical values onto the stack. The 16-bit integer math can handle from -32768 to +32767. The language, when written in C can easily be extended to cope with 32 bit numbers.

Having used approximately one third of the printable ascii characters to create a set of instruction primitives, that left the upper and lower case alpha characters to provide variables, user defined functions and other means for extending the language. Tiny BASIC and VTL-2 often used the uppercase alpha character to denote user variables - limited to 26, but that’s often enough for a simple application.

So my interpreter and virtual machine began to take shape and it has been ported to almost every microcontroller I have worked with over the last 7 years - including ATmega, ARM M4, M7, MSP430 and most recently my custom cpu loosely based on Woz’s Sweet-16.

In the early days of developing this shorthand language - I looked for an easy acronym to name it. So I called it SIMPL - serially interpreted minimum programming language.

I have parked it and revisited it so many times over the years - but it still comes back to haunt me. Right now I am writing a self hosted assembler to run on my custom cpu simulator, using SIMPL to allow me to write in a very concise, shorthand assembly language.

I grew up with the Z80 and I like lots of registers, so if I want to call them AF BC DE HL IJ MN O and P etc where A is the accumulator, H and L access memory, I and J are loop counters and P is the program counter, R can be the return stack pointer and S can be the stack pointer, T can be the top of stack and N is the Next of stack - I find that to have a lot more mnemonic value than R0 to R15.

More ramblings later - I have code to write.

2 Likes

The BASIC Onion got me thinking, I have yet to a see
a real recusive computer design.
level 0 R=R op R R0 is @(R1)
1 R=#
2 move R R on condition.
PC is R7
level 2
Indexing
level 3
floating point/traps
level 4
user stuff.
each level nests down to a lower program space
and memory speed.
Ben,

Interestingly, this is also where machine language started, with single character instructions, which were also mnemonic. E.g., the Univac I (using decimal numbers encoded in excess-3 format for operands, but integer/fixed format) or quite similarly the LGP-30 (Stan Frankel, the designer of the LGP-30, came from Eckert’s and Mauchly’s ENIAC team, which may explain some of the similarities).

P.E., the instruction set of the LGP-30 looks like this:

instr - meaning
b - bring from memory (load into AC)
h - hold and store (deposit AC in memory)
c - clear and store (deposit AC and set it to zero)
y - store address (store AC as operand) – “yank”?
u - unconditional transfer (jump)
r - return address (stores PC+2 as operand at given address)
t - test (conditional transfer on AC negative)
z - stop (break points may be specified by bits in track part of operand)
p - print
i - input
a - add
s - subtract
m - multiply (most significant bits of result in AC)
n - multiply (least significant bits of result in AC)
d - divide
e - extract (mask, logical AND)

Speaking of the LGP-30, operands were references to memory addresses only. Now replace “memory address” by “variable”…

1 Like

NoLand, I became aware of this a few years back when I studied the EDSAC machine - which used a very similar notation of single character mnemonics.

I was not aware that this notation perpetuated through to the LGP-30, but it makes sense because so many of those 1945-1955 era machines had a common ancestry.

Maurice Wilkes (EDSAC) had travelled to the US in August 1946 for the Moore School Lectures, where he became instructed in the works of von Neumann, Eckert and Mauchly. Many of the early machine developments arose from the engineers that had attended that summer school workshop.

In that immediate post war period, there would have been a lot of surplus military equipment available including teleprinters and paper tape readers. It would have been obvious to the engineers of that time to repurpose this equipment for the data entry, printed output and paper tape storage of computer programs.

As the teletypewriters had a simple 5-bit Baudot code with letter shift and figure shift - the characters would consist of uppercase letters and a few punctuation symbols. From this limited character set, the early programmers would have to come up with a meaningful representation of the instruction set.

As all coding was done by hand, written out on a coding sheet before being handed to a data entry clerk, the simpler and clearer the mnemonics, the fewer mistakes would be made.

One of the things I did with SIMPL was to use it to allow me simulate the EDSAC instruction set and create a very primitive assembler, using the SIMPL interpreter for numerical and instruction entry.

It is a relatively easy process to adapt the interpreter for assembly code entry, the advantage with assembly language being that there is always only one instruction per line of code. With a machine like EDSAC or LGP-30, all operations were conducted on the accumulator and a memory reference, which was a huge simplification.

Compare this to a modern x86 or ARM instruction where there may be multiple register and memory operations all encoded into the one instruction. This is why almost nobody hand codes in ARM assembly language these days!

Having spent several of my teenage years hand coding in Z80 assembly language, and entering hex codes into a monitor program with a hex keypad, you slowly built up an intimate knowledge of the hex codes for the specific microprocessor. This seemed a useful skill at the time, but there is a limit to what the average human brain can retain.

It was said that Steve Wozniak was so familiar with the 6502 codes that he could write hex directly to memory and work out all the relative jumps as he went along. Mind you Woz certainly does not have an average human brain!

2 Likes

I took the diversion this morning to discuss Wozniak’s “Sweet-16” virtual machine.

This was to illustrate the relative simplicity of the design of an interpreter like this - using 6502 assembly language that will be familiar to many on this forum.

Inspired by the simplicity of Sweet-16, I wondered whether I could design a 16 bit machine with an ISA loosely based on Sweet-16.

The answer was yes, and I have a simulator coded up in fewer than 60 lines of C code - running on the Arduino compatible Teensy 4.0.

Reading through the description and listing on the web page, reveals the elegance of Woz’s design.

Sweet-16 was used for moving blocks of data around in the 16-bit memory space on the Apple II, and for operations such as string comparisons and manipulations.

It’s not a general purpose VM, it only has ADD, SUB, Compare, INC and DEC. One obvious extension would be to add AND, OR and XOR to extend the logical capabilities.

However there are just not enough instruction slots available in the current scheme to allow these instructions to apply universally across all registers. Some compromise is required or a wider instruction format will be needed.

So with a bit of re-jigging, and extending the instruction to a 16-bit width, I came up with the following scheme:

Register OPS-

     0n        ---       --     Non-Register Ops
     1n        SET       Rn     Constant  (Set)         Rn = @(PC+1)
     2n        LD        Rn     (Load)                  AC = Rn
     3n        ST        Rn     (Store)                 Rn = AC
     4n        LD        @Rn    (Load Indirect)         AC = @Rn
     5n        ST        @Rn    (Store Indirect)        @Rn = AC
     6n        POP       @Rn    Pop  AC                 AC = @Rn  Rn = Rn - 1
     7n        PUSH      @Rn    Push AC                 @Rn = AC  Rn = Rn + 1 
     8n        AND       Rn     (AND)                   AC = AC & Rn 
     9n        OR        Rn     (OR)                    AC = AC | Rn 
     An        ADD       Rn     (Add)                   AC = AC + Rn
     Bn        SUB       Rn     (Sub)                   AC = AC - Rn
     Cn        INV       Rn     (Invert)                Rn = ~Rn
     Dn        DCR       Rn     (Decrement)             Rn = Rn - 1
     En        INR       Rn     (Increment)             Rn = Rn + 1
     Fn        XOR       Rn     (XOR)                   AC = AC ^ Rn

The first 7 instructions involve memory access, and the remaining 8 utilise the ALU operations on the accumulator and the specified register n.

Non-register OPS-

 00        BRA    Always                        Target = IR7:0
 01        BGT    AC>0                          Target = IR7:0
 02        BLT    AC<0                          Target = IR7:0
 03        BGE    AC>=0                         Target = IR7:0
 04        BLE    AC<=0                         Target = IR7:0 
 05        BNE    AC!=0                         Target = IR7:0
 06        BEQ    AC=0                          Target = IR7:0     
 07        JMP    16-bit                        Target = @(PC+1)
 08        CALL   16-bit                        Target = @(PC+1)
 09        RET    Return
 0A        ADI    Add 8-bit Immediate           R0=R0+Immediate IR7:0
 0B        SBI    Subtract 8-bit Immediate      R0=R0-Immediate IR7:0
 0C        OUT                                  putchar(AC), port = IR7:0
 0D        IN                                   AC = getchar(), port = IR7:0
 0E        JP@                                  BRA (R0)
 0F        NOP                                  AC &= AC

The non-register ops make use of the lower byte of the Instruction register IR7:0. I call this the payload byte - and it can contain an 8-bit immediate, a port address or an 8-bit index for the branch operations.

The payload byte is generally not used for the register operations - but it could be used as an 8-bit offset to extend the flexibility of the addressing modes.

Instruction 0E is used for table look-up with a jump to the acquired address.

Instruction 0F is a place-keeper and initially intended for NOP, but there exists the possibility to use the payload byte as 8 individual “microcoded” control signals - to be used to operate directly on the accumulator for operations such as Clear, Complement, Left and Right shifts etc - this has been inspired by the PDP-8 “Operate” instruction scheme.

2 Likes

Shift instructions and the carry bit seem to be missing.
If you use memory mapped io, in/out could be load
store byte pointer, using the carry bit for hi/low byte,
Add EI,DI,and HALT you have complete computer,

There’s a nice quip about the Manchester Mark 1, which was programmed in machine code, using printable characters (there being rather a small character set and rather few not being printable):

Binary zero, represented by the forward slash, was the most common character in programs and data, leading to sequences written as “///////////////”. One early user suggested that Turing’s choice of a forward slash was a subconscious choice on his part, a representation of rain seen through a dirty window, reflecting Manchester’s “famously dismal” weather.

Because the Mark 1 had a 40-bit word length, eight 5-bit teleprinter characters were required to encode each word. Thus for example the binary word:

10001 10010 10100 01001 10001 11001 01010 10110

would be represented on paper tape as ZDSLZWRF. The contents of any word in store could also be set via the teleprinter’s keyboard, and output onto its printer. The machine worked internally in binary, but it was able to carry out the necessary decimal to binary and binary to decimal conversions for its input and output respectively.