MINT - a Minimal Interpreter for resource limited cpus

Way back in 2013, when I started working with the STM32 ARM on a motion control project, I needed a simple serial connection so that I could control the fledgling product.

I settled for a very simple set of commands, each using a capital letter, and a single numerical parameter, so 100U would move the machine head 100mm up and 100D would move it down by the same amount.

As the project developed, more commands were added, again with capital letters, chosen for their mnemonic value.

About the same time, I read about the first Cambridge EDSAC which had an instruction set based on the 5 bit baudot code used by punched tape teleprinters, again using upper case letters for their mnemonic value.

So I pondered whether I could create a very lightweight programming language using single character ascii commands, chosen for their mnemonic value, brevity and human readability.

So I pottered along with some ideas and created a very simple interpreter, coded in C which allowed me to sequence a train of commands sent to the STM32 hardware. It provided basic interaction with the hardware from a serial terminal application running on a laptop.

Having had a little experience with Forth, my ideas converged on a Forth-like RPN language. But as a lazy programmer, I wondered if I could throw away 75% of Forth and still have something of value. So out went the dictionary and the multi-character parsing that went with it. The compile mode and 16 bit addressing were also ditched, leaving a very simple bytecode interpreter that used the printable ascii character set as it’s instruction set.

30 instructions, chosen from appropriate mathematical, logical and punctuation characters.

26 user routines identified by a single uppercase letter

26 user variables identified by a lowercase letter.

If this is starting to sound familiar, it is the Tiny BASIC equivalent of Forth. Unlike VTL that has features in common with BASIC, the MINT interpreter has a lot in common with RPN stack-based languages.

Minimalist languages have been around since the start of the electronic computing era. I was delighted to discover that way back around 1969, a lightweight character based interpreter called MUSYS was written for the PDP-8, by Peter Grogono, in order to simplify the task of electronic music composition. He later evolved this into MOUSE - a language for microcomputers around 1980. Having recently bought a copy of Grogono’s book to obtain the source code, I can see that MINT is very close to MOUSE in functionality, but about 20% smaller in terms of source code.

MINT is also quite similar in nature to desk calculator (dc).

MINT uses a look-up table that converts the 96 printable ascii characters into a jump address to a page of memory that contains the primitive routines, and the routines to handle numbers and variables.

The most commonly used routines (arithmetical and logical functions) are accessed on a single 256 byte page, less frequent routines have an intermediate jump to allow access to the next page. This method was chosen to provide the fastest means of accessing a routine on most 8-bit processors. Arithmetic is 16-bit integer and there are routines to accept numbers in both decimal and hexadecimal.

MINT is a very terse language with only 30 primitive instructions. These provide arithmetical and logical instructions, loops, arrays and conditional branching.

There is a data stack and a return stack, just like Forth. However, there are 26 user “registers” available, and after some months of using MINT, it became apparent that using these registers for temporary storage can save a lot of the stack juggling operations that are so common in Forth.

In 2021 I collaborated with John Hardy and Craig Jones, both from Melbourne Australia and we created the first fully documented version of MINT. Initially for the Z80, as John created the TEC-1 Z80 SBC, way back in 1983, and MINT provides an alternative means to program the TEC-1 from a serial terminal other than in Z80 hex codes.

We are hoping to port MINT to several of the classic 8-bit micros, including 6502, 1802, 6800/6809 as well as PIC and AVR and 8051. We are also looking at a cut-down version of MINT providing the bare minimum for a 16-bit VM in about 1Kbytes.

There is also a C simulator for the MINT interpreter which runs to about 80 lines of fairly dense code.

Looking forwards, we see MINT as being a very lightweight 16-bit virtual machine, which can be easily implemented on any resource limited cpu.

It’s bytecode instruction set is human readable and we see it as a means of passing small programs between systems that do not necessarily share the same CPU. Some might call this a security nightmare, but I look at it as an opportunity.

I have a colleague in Malaysia who deals with commercial greenhouse automation systems using distributed 8051 based control nodes. MINT can provide a command shell to update the system to provide a control environment that will respond to variations in temperature, lighting and mositure/humidity.

On github you can find MINT here:

5 Likes

Thanks for the new topic, @monsonite (previously MINT was mentioned here) - it does look very interesting to me, a small Forth with a small portable virtual machine. Might yet be applicable to one or other of the OPC machines!

I will publish the C simulator in due course. I look at MINT as a simple low overhead tool that gets you out of cpu specific machine code.

For 6502 Gurus, that can port VTL from 6800 to 6502, the mechanics of MINT will be trivial.

I’m still looking for an 1802 Guru, because my 1802 SBC is not yet working.

Ed,
When we met in Cambridge in 2017 it was little more than an idea.
John Hardy and Craig Jones have been instrumental in putting flesh onto the bare bones.

1 year on, I can say that it is not a user friendly language for beginners because of its RPN and very terse commands.

So we are now looking at ways to use it as a tiny VM in under 1K bytes, for code portability, brevity and simplicity. We might even end up with a python app that spits out MINT code.

I took on the last 2 Christmas challenges - just to prove to myself that MINT can be viable for these simple applications.

Sometimes I play with Arduino compatible boards, but when it took 22K bytes on the Teensy 4 just to flash an LED, I felt that there must be a better way.

I wonder, does it feel a bit like programming an RPN calculator? I imagine it might.

Most of the time you get stuck on a function to manipulate data, but not usually on simple maths.

This one converts an 8 bit number on the stack into individual bits so as to print out graphics for the Christmas Tree challenge last year.

:S 32\E;
:X 42\E;
:Q "(42\E)0=(32\E);						
:P b!8(128b@&128=Qb@{b!);

S prints a space, X prints an asterix, Q and P work together to analyse a number bit by bit to decide whether to print space or star. b is a local variable and { is a left shift

Like I said earlier, it is very terse.

2 Likes

I know it’s a stretch (and not a dis of MINT!), but this reminds me of TECO, which started life as a powerful text editor, but became a programming language in its own right, most famously for being the implementation language of the first EMACS. TECO didn’t stop at printable characters—there weren’t enough of those—and quickly added control characters to its command set. Formatting for readability? No. Indentation? Maybe. And so TECO programs gained a playful reputation among its fans as resembling “line noise,” which many of us are familiar with from the TTY days.

1 Like

Actually it reminds me of just about every intermediate code for a portable compiler.For example:

! OR               G  ALIAS            c MCODE
" JUMPIFD          H BEGIN             d DIM
# BNE              I unused            e EVENT
$ DEF              J JUMP              f FOR
% XOR              K FALSE             g unused
& AND              L LABEL             h ALTBEG
' PUSHS            M MAP               i INDEX
( unused           N PUSHI             j JAM
) unused           0 LINE              k RELEASE
* MUL              P PLANT             l LANG
+ ADD              Q DIVIDE            m MONITOR
- SUB              R RETURN            n SELECT
. CONCAT           S ASSVAL            o ON
/ QUOT             T TRUE              p ASSPAR
: LOCATE           U NEGATE            q ALTEND
; END              V RESULT            r RESOLVE
< unused           W SJUMP             s STOP
= unused           X IEXP              t unused
> unused           Y DEFAULT           u ADDA
? JUMPIF           Z ASSREF            v MOD
@ PUSH             [ LSH               w SUBA
A INIT             \ NOT               x REXP
B REPEAT           ] RSH               y DIAG
C JUMPIFA          ^ PROC              z CONTROL
D PUSHR            _ SLABEL            { START
E CALL             a ACCESS            | ALT
F GOTO             b BOUNDS            } FINISH

(the opcodes being explained in I-code V1.3 Working-Notes )

I don’t consider myself to be a guru, but I think I have sufficient skill … time and effort are the limiting factors for me. I feel like I want to get a 6502 version running before I wirite any MINT programs of my own.

But looking at the github you linked, it appears that the Z80 versions may be outdated and buggy (please forgive me if that’s not the case, as my Z80 chops are a bit fuzzy). Can you link me to the source for the latest and greatest Z80 version? For me, the best possible reference would be a .lst file, with all of the macros and includes expanded … my work style is a bit unconventional, and that would be the least stressful path for me.

Thanks in advance.

Michael,

The latest repository, maintained by John Hardy is here:

Our first task was to get MINT on a couple of Z80 SBCs - namely the TEC-1, which John co-designed for the Australian Electronics magazine “Talking Electronics” way back in 1983.

Here, I run MINT on a couple of RC2014 Z80 boards.

MINT was intended to make a tiny RPN language that was “Forthlike” rather than “BASIClike”.

It forms a 16-bit VM on the target CPU, in a similar way to Wozniak’s SWEET-16, but with about 32 instructions rather than just 16.

Hopefully the Read_Me will guide you through the BASICS.

I started a 6502 version using Alexandre Dumont’s 6502 primitives, but I failed to get the basic character interpreter loop to run - then work got in the way.

The main principle is that the interpreter reads the next ascii character in the text input buffer, the ascii code of the character indexes into a look up table, and that table supplies a 16 bit jump address to the start address of the primitive routine.

Every printable ascii character read will force a jump or a call to a given action routine.

The arithmetic and punctuation characters are generally associated with arithmetic, logic and stack operations.

Lowercase characters a-z form 16-bit user registers

Uppercase characters A-Z are used to invoke user defined routines.

A data stack and a return stack need to be supported.

Arithmetic is normally 16-bit integer decimal, internally handled as binary, although a routine will allow hexadecimal to be entered, converted to binary and printed out.

The key is to get the interpreter loop as efficient as possible, i.e. lowest possible overhead. SWEET-16 IIRC had quite an efficient interpretive loop.

2 Likes

Gotcha, Ken, and thanks. I felt the unexplainable urge to play with the Z80 source, starting about a week ago, and I can say that I’m off to what feels like a favorable start. I also watched John’s 3-hour video, where he goes through the internals exhaustively.

The few questions I have so far mostly deal with minor discrepancies between the documentation and the source, e.g. how negative decimal literals are parsed (leading ‘-’ or trailing ‘_’) and whether or not to add leading 0s when printing decimal numbers < 10000. I really should fire up a Z80 emulator and play around with the command line and your code examples to get a feel for some of the edge cases.

This looks like a good enough place to post updates on my progress and queries, when and if, no?

1 Like

Michael,

The decimal integer print routine was one of my contributions early on in the project. At that time I was looking for minimum code size, so that’s why it does not suppress the leading zeros.

Originally, the jump table only created an 8-bit jump address - this again was for minimum code size and fastest despatch, and inspired by SWEET-16.

The most commonly used primitives were packed into about 200 or so bytes, on a single page beginning at 0x200. The remaining primitives had a forwarding address jump, placed towards the end of this page, which could access pages at 0x300 and 0x400. The multiply and division routines were considered to be low priority, and placed on page 3. The overhead of an extra jump, is negligible compared to the time spent multiplying or dividing 16-bit integers.

John added a lot of alternate function codes, all preceded by the \ character. The extra functionality provided meant that it was no longer practical to retain the 8-bit jump table, and jump addresses were extended to 16-bits, making them more like the Forth model.

Whilst this move enhanced the functionality, it kind of took MINT away from the original concept of a very lightweight 16-bit VM, with a limited instruction set based on human readable ascii codes.

The idea was that the interpreter could be used as a “lingua franca” or universal low level language, so that code could be exchanged between any cpu that hosted the MINT interpreter.

This would allow code to be shared between 6502, Z80, 6809, 1802 etc.

I am reminded of the Tiny BASIC interpretive language which implemented the same VM on a range of 8-bit cpus:

Universal languages have been tried before - and not really gotten far. Mostly because not everyone likes that particular universal language. For example; I am not a fan of Forth or other RPN style things, so while I did look at this when I first saw it (on Facebook), I more or less glossed over it in a sort of “that’s nice” sort of way.

So you can never please everyone…

And even then, they’re different (speaking a someone who’s just recently implemented a TinyBasic on the 6502). Going back to the mid-70’s, you’d find yourself with many different dialects of TinyBasic - a maze of twisty passages, all different… Each with their own little additions (or deletions). Some had FOR/NEXT, some not, some supported multiple statements per line, some not, and so on…

I’d argue that BASIC is as universal as you might get, but then, like spoken “english” you have dialects… If ye ken whit ah meen. So lets naw cause a stramash aboot it, dinnae fash and lets chill and enjoy…

-Gordon

1 Like

Well, MINT belongs completely to you and your team, so as long as you stick together, you’re the first and final word on the standard.

I already have some tight 6502 subroutines for printing strings, decimal numbers and hex numbers, but my decimal print has leading zero suppression built-in, so I may have to “dumb it down” if that’s going to be an issue:

; *************************************************************
printstr:               ; 29 bytes
    pla                 ; print imbedded \0 terminated string
    tay                 ; get low part of (string address-1)
    pla                 ; get high part of (string address-1)
    sta  H              ;
    lda  #0             ; set up string pointer
    sta  L              ;
    beq  prstr3         ;
prstr2:                 ;
    jsr  putchar        ; output string char
prstr3:                 ;
    iny                 ; advance string pointer
    bne  prstr4         ;
    inc  H              ;
prstr4:                 ;
    lda  (L),y          ; get string char
    bne  prstr2         ; keep printing if not NUL
    lda  H              ;
    pha                 ; set up return
    tya                 ;
    pha                 ;
    rts                 ; proceed at machine code following NUL
; *************************************************************
printhex:               ; 26 bytes
    lda  1,x            ;   (  --  ) print TOS as 4 digit hex
    jsr  pr2hex         ;
    lda  0,x            ;
pr2hex:                 ;
    pha                 ; print A as 2 digit hex number
    lsr                 ;
    lsr                 ;
    lsr                 ;
    lsr                 ;
    jsr  pr1hex         ;
    pla                 ;
pr1hex:                 ;
    and  #$0f           ; print low half of A as hex digit
    cmp  #10            ;
    bcc  prdig          ;
    adc  #$66           ;
    bne  prdig          ; (always)
; *************************************************************
printdec:               ; 44 bytes
    lda  1,x            ;   ( n -- 0 ) print TOS as decimal
    bpl  prdec2         ; check for < 0
    lda  #"-"           ;
    jsr  putchar        ; print negative sign
    jsr  neg            ; flip TOS to positive
prdec2:                 ;
    lda  #0             ; remainder (current digit)
    clv                 ; (sets if quotient > 0)
    ldy  #16            ;
prdec3:                 ; 16-bit divide by ten {
    cmp  #5             ;   partial rem >= radix/2?
    bcc  prdec4         ;
    sbc  #133           ;   yes: update rem, set V and C for
    sec                 ;     non-zero quotient
prdec4:                 ;
    rol  0,x            ;   new quotient gradually replaces TOS
    rol  1,x            ;
    rol                 ;   new remainder gradually replaces A
    dey                 ;
    bne  prdec3         ; } continue 16-bit divide
    bvc  prdig          ; quotient > 0?
    pha                 ;   yes: one or more digits must be
    jsr  prdec2         ;     printed before this one, so
    pla                 ;     recurse to handle them first 
prdig:                  ;
    eor  #"0"           ; convert binary to ascii digit
    jmp  putchar        ; print the digit
; *************************************************************

The data stack pointer is X and the return stack pointer is S, because 6502.

My dispatch routine isn’t done yet, but I’m trying to keep 8-bit entries in my table by storing the offset into the primitive page divided by two, and multiplying by two before doing the RTS dispatch a la Sweet16. This means that I have 510 bytes of space to access, but all of my primitives must start on an odd-numbered address (because 6502). My initial thoughts are that a scattering of padding NOPs between primitives to keep the alignment correct is a small price to pay for the extra real estate:

over_:                  ; 8 bytes
    ldy  2,x            ; % ( n1 n2 -- n1 n2 n1 )
    lda  3,x            ;
    jmp  pushay_        ;
    nop                 ;
and_:                   ; 12 bytes
    lda  0,x            ; & ( n1 n2 -- n1&n2 )
    and  2,x            ;
    tay                 ;
    lda  1,x            ;
    and  3,x            ;
    jmp  dropay_        ;
drop_:                  ; 6 bytes
    inx                 ; ' ( n --  ) drop TOS
    inx                 ;
    bmi  inv2           ; next
    bpl  crash          ; (underflow)
1 Like

Michael,

It sounds like you have a lot of the essentials that can be borrowed from other projects.

That’s a neat idea using an 8-bit table to address 510 addresses.

I think that once I got the dispatch routine working and decimal number conversion to binary and placed on the stack, plus decimal printing, I got integer arithmetic running fairly shortly afterwards.

Unlike Forth, MINT only needs whitespace to signify the end of a number string entry. Almost all other “words” can be entered without spaces, but spaces improve human readability! Once a routine has been debugged spaces spaces can be removed.

123 456+.

This my popular test to show that ADD was working

It should reply:

00579

I have aspirations of using MINT to create a primitive assembler or at least a monitor for direct examination and modifying memory.

There are several bytecode oriented languages, and MINT was just a mash-up of the best ideas from a few of them.

Whilst we implemented traditional stack manipulation words like DUP, DROP, SWAP and OVER, we found that having 26, general purpose 16-bit registers in zero page RAM (or equiv) meant that MINT used far fewer stack juggling operations, when compared to Forth. This then begs the question, how deep does the stack really need to be, and can we use a technique similar to the RCA 1802, where we have 16 general purpose 16-bit registers, which can be easily be incremented or decremented and used to set up multiple stacks and multiple address spaces.

With memory divided into into 16 x 2K word chunks, the 26 user words A-Z could have a different context in each memory space.

Hopefully there might be incentive to target 6800/6809, 1802 and possibly even an emulated Datapoint 2200 or PDP-8!

Probably the best target is a MSP430 - which at 16-bits, with a very orthogonal small instruction set, most of the primitive words are just one or two instructions. I wonder how the 65816 would fair as a target?

PDP-8 was used with a byte code language by Peter Grogono in about 1968ish for very early digital control of music synthesisers and electronic music composition. Coding in MUSYS was a lot easier than coding in PDP-8 assembly language!

The MUSYS language later evolved into MOUSE, for which there is a Z80/8080 version that runs under CP/M.

Mouse was an early Inspiration for MINT, but I wanted to make it more “Forthlike” and more human readable.

The main philosophy with MINT is to Keep it Simple, Small And Something Steve Wozniak Would Willingly Work With.

I call this the KISS ASS WWWWW principle. :slight_smile:

I am keen to follow your progress with the 6502 - although you might need 2K of fallow memory!

The BCPL CINTCODE VM is stack-based. It also has a 3-deep register stack, although the operations that “push” only move A to B. There are separate instructions to copy A into C and back again. There is no ‘pop’ instruction to move B back into A. The compiler does the rest to move data from RAM into the register stack and back again.

And, AIUI, the boffins who started up Inmos in the UK (some of who hailed from the same university as BCPL), and created the Transputer asked this question and the answer was 2, maybe 3. The transputer is/was a sort of general purpose 32-bit CPU that had a hardware threading engine and high-speed links for inter CPU communication. The assembly code was a stack based machine with 3 registers (or a 3-deep stack if you prefer) and there are similarities between that and BCPL Cintcode…

-Gordon

Yes - I understand the Transputer only had a 3 level stack. Similar to the HP 35 etc range of scientific calculators.

I had a school friend from George Heriots and then Heriot Watt University who started his career at Inmos in 1986. Prior to that he worked vacations at a ATM and computer terminal company in either Glenrothes or Dunfermline.

Ken

Michael,

I discovered tonight that we have another 6502 enthusiast working on MINT.

You might want to check out his code here:

His name is Alvaro G S Barcellos - he has been a long time member on my Minimalist Computing, Facebook Group and he has suggested that he is looking for co-operative development.

https://www.facebook.com/groups/441341066956027/user/1819087666

regards,

Ken

Ah, shoot! I didn’t know that there was any active 6502 porting activity, or I would have checked in first, so as not to step on any toes. I don’t Facebook, and I have dubious “co-operative” skills, but I wouldn’t mind having a private conversation with AGSB to help me decide how to respectfully proceed. If you don’t mind, could you make him aware that my personal email address is barrym95838#yahoo%com, after you correct the punctuation to something more intuitive? Thanks.

Michael,

I have passed your email address onto Alvaro. Hopefully you will find some common ground.

Ken