Adventures with Cowgol

Adventures with Cowgol

I've written two languages so far for the MicroBeast: AntForth, which I use heavily every day for experimentation, and another Lua-like language which is part of a super-secret future project that is still under wraps. But I'm always on the lookout for new projects, and fancied another language. Maybe a Prolog? A Lisp perhaps? Then I re-discovered something I found a year ago and forgot all about until now: Cowgol 2.0.

Cowgol comes from the mind of David Given, a prolific retro-tech enthusiast, so we know we're in safe hands. Cowgol is inspired by Ada - a language I remember mocking as a greenhorn developer for being too formal,verbose, and rigid. Thirty years later, it seems very attractive because it is formal, verbose and rigid.

Ada was the 1980's equivalent to Rust. Like Rust, if it compiles in Ada it will probably run. Ada is safe. Very safe. The C compiler will happily let you shoot yourself in the foot - the Ada compiler won't even let you touch the gun. Its whole raison-d'etre (for it indeed has French origins) is to stop expensive fighter jets and cruise missiles falling out of the sky.

Ada was designed by committee, and that committee ticked every box on the Language Features We'd Like form. But unlike other similar languages (Scala, say) it is not a total mess. Evidently early versions of Ada often didn't bother to implement large parts of the language spec, which must have made safety-critical programming interesting, but the language is still evolving and everybody seems to agree that the Ada 2012 standard is a particularly fine vintage.

For another perspective, here's an article from 12 years ago about Ada, written by a Mr David Given.

So I'll be putting Ada on my "languages to learn list", but in the meantime David is a decade ahead of me and already on version 2.0 of his Cowgol toolchain. It supports z80. It supports CP/M. It can self-host. It has lovely syntax. I think the path forward is obvious, and inevitable.

Building the cowgol toolchain

Everything we need is in the Cowgol repo. Make sure you get the right one: there's another one called Cowgol_on_CP_M which is derivative, and based on the older cowgol 1.0.

The toolchain builds under linux, as is proper. We're going to need to install a lot of dependencies, and I'll save you a bit of time: the first thing you need to do is to go to http://vincent.riviere.free.fr/soft/m68k-atari-mint/ubuntu.php and install the cross-mint-essential.

For the remainder of the deps, doc/build.md has the command we need:

    apt install moreutils lua5.1 libz80ex-dev flex libbsd-dev libreadline-dev \
      bison binutils-arm-linux-gnueabihf binutils-i686-linux-gnu \
      binutils-powerpc-linux-gnu binutils-m68k-linux-gnu binutils-m68k-atari-mint \
      qemu-user gpp 64tass nasm python3

Then it's just a matter of:

make

This gets us an alarming number of binaries in /bin:

According to building.md the format is $TOOLNAME-for-$TOOLCHAIN-with-$TOOLCHAIN although I wonder if he meant $TOOLNAME-for-$PLATFORM-with-$TOOLCHAIN.

The various tools are:

Tool name Purpose
cowfe FrontEnd: parsing, type checking, ASTs, allocation
cowbe BackEnd: generate machine code, as .coo file
cowlink Linker: link multiple .coo files into 'assembly' file
cowwrap ??? turn a .cos file into a .coo file

File formats:

File extension Purpose
.cow Cowgol source file
.cob Intermediate file. Output from cowfe, input to cowbe
.coo Chunked format containing both "assembly" and binary markup
.cos An annotated text file format that can be converted into .coo

Targets (as defined in the README.md):

Target Notes Self-host?
Z80 and 8080 on CP/M Yes
6502 and 65c02 on BBC Tube Yes
6303 on Fuzix No
6502 bytecode on BBC Tube this time a bytecode VM No?
80386 on Linux Yes
ARM Thumb2 on Linux Yes
PowerPC on Linux Yes
68000 on TOS, Amiga CP/M, or Linux m68k Partial syscalls No
8086 on DOS Small-mode .exes No
PDP11 on V7 Unix No
C Generic, terrible
Microsoft BASIC Terrible. Joke. No

There are many more platforms with only partial support, not listed here.

Toolchains (as defined in building.md):

Toolchain Notes Built With
bootstrap a C version of the compiler, used to build the first stage compiler C
ncgen targets C bootstrap
nncgen targets C ncgen
lx386 targets Linux 80386 binaries nncgen
cpm targets CP/M 8080 binaries nncgen
cpmz targets CP/M Z80 vinaries nncgen
bbct targets BBC Tube 65c02 binaries nncgen
bbcti targets BBC Tube 65c02 bytecode nncgen
bbct6502 targets BBC Tube 6502 binaries nncgen

If we make our first goal "compile something on Linux that will run on Z80 CP/M" then we should be looking for something like cowfe-for-80386-with-ncpmz - and indeed this file exists, but it isn't executable, which is weird. The file utility says it is "data" which is not helpful. Maybe I should be looking for cowfe-for-ncpmz-with-80386 ? Nope, doesn't exist.

The examples in building.md don't match any of the actual executables in bin - I'm guessing the docs came from the earlier Cowgol 1.0 and didn't get updated.

I'm a bit baffled at this point. There are a bunch of cowfe platforms like 16bit, 32bita2, 32bita that have hitherto gone unmentioned, which isn't helping. Looks like I'm not the only one, there's a github issue asking the same question, but unfortunately the provided answer answers a different question (how to build on z80 CP/M for z80 CP/M, which might prove interesting later).

Let's make a list of all the cowfes that are genuinely ELF 64-bit executables:

 find . -type f -exec sh -c 'file -b "$1" | grep -q "^ELF 64-bit.*Linux" && printf "%s\n" "$1"' _ {} \; | grep cowfe
 
./cowfe-for-32bita-with-nncgen
./cowfe-for-pdp11-with-ncgen
./cowfe-for-80386-with-nncgen
./cowfe-for-basic-with-ncgen
./cowfe-for-basic-with-nncgen
./cowfe-for-32bita2-with-nncgen
./cowfe-for-cgen-with-nncgen
./cowfe-for-pdp11-with-nncgen
./cowfe-for-6502-with-nncgen
./cowfe-for-cgen-with-ncgen
./cowfe-for-32bita-with-ncgen
./cowfe-for-80386-with-ncgen
./cowfe-for-16bit-with-nncgen
./cowfe-for-6502-with-ncgen
./cowfe-for-16bit-with-ncgen
./cowfe-for-32bita2-with-ncgen

Ah OK, so I guess with-ncgen means that's what the executable was built with ncgen, which is described as targets C, which I guess means it made a C file, which the build system compiled as a binary for this host platform.

Sanity check:

That is indeed an executable. Note that neither Z80 or CP/M appear in the for-X position, our options are obvious non-starters (basic, pdp11, 6502) or undocumented bafflage (32bita, 32bita2, 16bit). As for 80386 and cgen... well could be, who knows.

What about cowbes?

❯ find . -type f -exec sh -c 'file -b "$1" | grep -q "^ELF 64-bit.*Linux" && printf "%s\n" "$1"' _ {} \; | grep cowbe

./cowbe-for-80386-with-ncgen
./cowbe-for-6502-with-nncgen
./cowbe-for-68000-with-nncgen
./cowbe-for-powerpc-with-ncgen
./cowbe-for-8086-with-ncgen
./cowbe-for-pdp11-with-nncgen
./cowbe-for-65c02-tiny-with-nncgen
./cowbe-for-z80-with-nncgen
./cowbe-for-powerpc-with-nncgen
./cowbe-for-6502-with-ncgen
./cowbe-for-basic-with-nncgen
./cowbe-for-cgen-with-ncgen
./cowbe-for-8086-with-nncgen
./cowbe-for-65c02-with-ncgen
./cowbe-for-cgen-with-nncgen
./cowbe-for-65c02-tiny-with-ncgen
./cowbe-for-68000-with-ncgen
./cowbe-for-80386-with-nncgen
./cowbe-for-8080-with-ncgen
./cowbe-for-basic-with-ncgen
./cowbe-for-z80-with-ncgen
./cowbe-for-pdp11-with-ncgen
./cowbe-for-8080-with-nncgen
./cowbe-for-65c02-with-nncgen
./cowbe-for-6303-with-nncgen
./cowbe-for-thumb2-with-nncgen
./cowbe-for-6303-with-ncgen
./cowbe-for-thumb2-with-ncgen

Here we've got many more for-X options, including a promising looking for-z80. No mention of cpmz yet, I'm guessing we need to look at our cowlink options:

find . -type f -exec sh -c 'file -b "$1" | grep -q "^ELF 64-bit.*Linux" && printf "%s\n" "$1"' _ {} \; | grep cowlink

./cowlink-for-bbctn-with-ncgen
./cowlink-for-fuzix6303-with-ncgen
./cowlink-for-bbctn-with-nncgen
./cowlink-for-ncpmz-with-nncgen
./cowlink-for-lxthumb2-with-ncgen
./cowlink-for-lxppc-with-ncgen
./cowlink-for-basic-with-nncgen
./cowlink-for-v7unix-with-nncgen
./cowlink-for-lx386-with-ncgen
./cowlink-for-bbct-with-ncgen
./cowlink-for-bk10dx-with-ncgen
./cowlink-for-amigacpm-with-nncgen
./cowlink-for-ncpm-with-ncgen
./cowlink-for-cgen-with-nncgen
./cowlink-for-msdos-with-ncgen
./cowlink-for-lxppc-with-nncgen
./cowlink-for-ncpm-with-nncgen
./cowlink-for-fuzix6303-with-nncgen
./cowlink-for-bk10dx-with-nncgen
./cowlink-for-msdos-with-nncgen
./cowlink-for-lx68k-with-nncgen
./cowlink-for-v7unix-with-ncgen
./cowlink-for-bk10-with-nncgen
./cowlink-for-basic-with-ncgen
./cowlink-for-rt11-with-ncgen
./cowlink-for-lx68k-with-ncgen
./cowlink-for-bbct-with-nncgen
./cowlink-for-bk10-with-ncgen
./cowlink-for-lxthumb2-with-nncgen
./cowlink-for-cgen-with-ncgen
./cowlink-for-rt11-with-nncgen
./cowlink-for-ataritos-with-ncgen
./cowlink-for-ncpmz-with-ncgen
./cowlink-for-ataritos-with-nncgen
./cowlink-for-lx386-with-nncgen
./cowlink-for-amigacpm-with-ncgen

And there it is: cowlink-for-ncpmz-with-nncgen. So I'm going to hazard a guess that our cross-compile toolchain is:

Tool Binary Confidence
cowfe cowfe-for-16bit-with-ncgen LOW (github issue)
cowbe cowbe-for-z80-with-nncgen HIGH there are only two z80 choices
and nncgen > ncgen
cowlink cowlink-for-ncpmz-with-nncgen HIGH there are only two ncpmz choices
and nncgen > ncgen

Cross compiling HelloWorld

OK, enough sleuthing, let's test what we've got. As I'm somewhat deficient in the Cowgol language skills department currently, I hunted around for some examples. I didn't have to hunt far, there's an examples directory, with this little beauty:

include "cowgol.coh";

print("Hello, world!\n");

The included helloworld.cow

Sadly this blog doesn't (yet!) have a Cowgol syntax file, nor does it inderstand Ada.

Let's compile it:

Bah. It wants an output. It's probably a .cob based on our earlier reading:

Gah! WTF is cowgol.coh ? Come to think of it, what's a .coh ??

A quick find .. -name cowgol.coh returns a lot of hits, but there are some promising looking ones in the rt directory, and the out-of-date build examples have cowfe flags like -Irt -Irt/lx386/ so I guess this is the runtime and we want rt/cpm as the runtime runs on our eventual target system. Let's have a poke around first:

Looks run-timey to me. Let's have a look at cowgol.h then:

Hmm, just looks like a cowgol source file. Not sure what the difference is between .cow and .coh then, mebbe we'll find out later. Still no speaky da Cowgol, but this looks straightforward. We've got a character printer that's making BDOS calls, looks like it's doing some LF->CRLF conversion for us. HIMEM and LOWMEM appear to be ... pointers? Looks like HIMEM is the BDOS entry point, and LOMEM is...something else. All very plausible stuff.

There's a cowgol.cos in there, which is in the Secret Format. Seems to contain a bunch of low level utilities for logic operations and basic maths, I'll ignore it.

The common.coh that's included at the end of cowgol.coh isn't in this directory, so I guess it's in the one above (because of that -Irt). Sure enough:

A bunch of printy, number-converty helper routines. Let's try our compile again:

FFS! Wait, the ancient examples have trailing slashes on the includes, maybe that's important:

Victory dance. We made our first .cob file:

It's binary and reveals nothing of its nature, but there is a debugging tool we can use to inspect it, cowbdmp (mercifully free of the confusing with-X / for-Y nomenclature):

It goes on like that for quite some time. Presumably this means something to cowbe, let's find out! I'm going to assume that cowbe wants a .cob input and a .coo output file:

Output is somewhat alarming, but I guess it's just informational, as we got a helloworld.coo file out. That file's a mix of binary and assembler text fragments, and there's no pretty dumper for it, so let's just try to link it. The ancient examples link an RT specific .coo file from the .obj directory, so I'm going to attempt to do the same thing:

Yeeesss, hole in one! Let's have a look at the output:

        org 100h
        ld sp, TOP+128
        call f29___main
        rst 0


        ; print_char workspace at ws+7 length ws+1
f34_print_char:
        ld (ws+7), a
        cp 10
        jr nz, c1c_0005
c1c_0004:
        ld e, 13 
        ld c, 2 
        call 5 
c1c_0005:
c1c_0001:
        ld a, ( ws+7 ) 
        ld e, a 
        ld c, 2 
        call 5 
end_f34_print_char:
        ret


        ; print workspace at ws+4 length ws+3
f36_print:
        ld (ws+4), hl
c1c_000d:
        ld hl, (ws+4)
        ld a,(hl)
        ld (ws+6), a
        or a
        jr nz, c1c_0013
c1c_0012:
        ret
c1c_0013:
c1c_000f:
        ld a, (ws+6)
        call f34_print_char
        ld hl, (ws+4)
        inc hl
        ld (ws+4), hl
        jr c1c_000d
c1c_000e:
end_f36_print:
        ret
c1c_c0:
        db 72,101,108,108,111,44,32,119,111,114,108,100,33,10,0


        ; __main workspace at ws+0 length ws+4
f29___main:
        ld hl, LOMEM 
        ld ( ws+0 ), hl 
        ld hl, (6) 
        ld ( ws+2 ), hl 
        ld hl,c1c_c0
        call f36_print
end_f29___main:
        ret
TOP:
ws equ TOP+128
LOMEM equ ws+8
        end

Ah now we see where LOMEM comes from. Also TOP. Very BBC BASIC. Surprised there's no PAGE. Anyway that looks pretty decent, certainly tighter code than I was expecting. I think my favourite assembler sjasmplus might be able to handle that:

Indeed he can. Let's have a look at the final output:

A bit of stack setup, the HIMEM/LOMEM stuff, looping through our string with the CRLF conversion, it all seems to be there, and not too much extra baggage.

Let's see what iz-cpm makes of it:

Very nice! Only one test remains, let's copy it on to our Beast and run it there. I'll have to rename it (oh and haha, just realised that .com fits the Cowgol .co? pattern nicely).

Testing it on real hardware

Fantastic! That is all rather marvellous.

Native Cowgol compilation

While we've got the Beast powered up, let's check out the native compilation options. That earlier github issue indicated that we have a ./bin/dist/cmpz folder with all the z80 cp/m tools in:

Looks like we've got the cowfe, cowbe and cowlink tools, all the RunTime support files, and a demo text-based Mandelbrot program! That demo.sub file is a handy batch file that we won't use directly but it does tell us what commands we need to run:

I copied the file over and started working through the commands:

Oh dear, that's not a very good sign. After 10 minutes, no output. Either it is stonkingly slow, or it has crashed. We could try the same thing in iz-cpm and see if we get a different result:

Yes, works fine. So it's a MicroBeast issue rather than a Cowgol issue. When we ran it on the Beast it didn't even generate the "COWFE" banner, which is worrying. Maybe we can fix it in the emulator later.

What I can do straight away though is take the MANDEL.COB file that iz-cpm made and see if we can progress it any further on the MicroBeast:

Oh dear, that's not working either. This time iz-cpm fails us too:

Ah well, at least they're consistent! On closer examination, this would appear to be because the MANDEL.COB file is zero bytes, long:

So I guess cowfe didn't work under iz-cpm either. Let's give it one last try under RunCPM. After preparing a B drive with the cowgol dist in:

RunCPM gets a lot further: cowfe, cowbe and cowlink all completed but z80asm choked on the final assembly. I tried it with sjasmplus, which didn't like it either:

Looks like a bunch of out-of-range relative jumps. I changed all the offending JR instructions to JPs, and we at least get to see it run in RunCPM:

Looks like the linker made some out of range relative jumps. I replaced them all with JP instructions and tried again:

Beautiful. I copied the MANDEL.COB (that RunCPM made) back to the MicroBeast, to see if cowbe there would run:

Success! But will it link?

And finally:

So a partial success on the self-hosting front. cowfe doesn't work on the Beast for reasons unknown, and cowlink produces bad assembler sometimes. I copied mandel.cow back to our cross-compiling directory and tried building it there, and it's the same story:

Next time...

Next steps are to learn some Cowgol syntax and write something "proper". Watch this space!