Jump to content
IGNORED

Millfork - a new programming language for 8-bit machines


Retrac

Recommended Posts

I discovered something recently, and it seems like it might be of interest to maybe a few of you.  It has been discussed on AtariAge for other platforms, but I didn't see anything specific to the 2600. 

 

Millfork is a C-like language, with adjustments to make it particularly suitable for 8-bit machines, and it targets the 8080/Z80/6809 as well as its best supported target -- the 6502.  It has an optimizing whole-program compiler with some nice tricks that often surprise me at the quality of the code generated.  It is only a couple years old and it is a hobby project, but it has some users with NES games and demos in particular.  The developer is quite friendly and responsive to language-related issues.  There is no 2600-specific support to speak of.  The example program given is all of it at the moment which dos the classic coloured bars and doesn't really demand anything fancy.  (I've done the same thing in C myself, as have others here, I think.)

 

Still, I've been writing some scaffolding for another project and, on a whim, I thought I'd try to cram some Millfork code in, just to see if it fits and works.  I got carried away and ended up rewriting essentially everything in Millfork.  So, I guess it was successful!    At this point, with some caution, I'd suggest it is possible to write game logic and so on in this.  Only a few dozen lines in my graphics kernel are in assembly. Though it doesn't actually do anything yet, besides display a simple monochrome bitmap.  Similar to my previous code posted, except in a high level language.  With fancy LZSA compression and a framebuffer in RAM :)

 

image.thumb.png.7609507312c26a441102d74b66e01e26.png

 

Speaking of which, cartridge RAM is a little awkward, but I have some macros that improvise a tolerable solution.  (The optimizer does like to cause a headache though.  INC is a problem.  Careful.  Write short routines, annotate them with volatile or inline and so on as necessary.)  Here's some 3E bankswitching nastiness which is not at all readable.  (Sorry.)  It takes a variable and writes to either $3E (bank_ram_reg) or $3F (bank_rom_reg) depending on what segment (>$80 is RAM) it is placed in.  It also extracts one of the 4 3E+ slots and writes that into the top 2 bits so it all bankswitches good and right.  And memory_barrier() is a hint for the optimizer not to elide or rearrange this operation:

 

macro void swap (void ref v) { 

  if bool(v.segment & $80) {
    bank_ram_reg = (v.segment & $7f) | ((v.hi & %0000_1100) << 4)
  }
  else {
    bank_rom_reg = v.segment | ((v.hi & %0000_1100) << 4)
  }
  memory_barrier()
}

 

Gives me a chance to show off its optimization abilities:  That's a lot of code above but it becomes, for something in ROM at least:

LDA #1 | ((hi(mydata)) & ($C)) << (4)     // the assembler can handle that, it's an immediate
STA $3F

Because Millfork is so low-level it lets you specify the locations of things literally.  There is fairly seamless support for inline assembly.  You can switch in and out of assembly within a function, or specify the whole function is written in assembly and then invoke some Millfork macros within your assembly. 

struct mystruct_t { 
    byte a
    word b
}

array (mystruct_t) foo[16] @ $F043                                    // 48 bytes in memory starting at f043
asm void myfunc(byte register(a) char) !preserves_x @ $FCD2 extern    // external assembler function that takes a byte in A reg

asm void myotherfunc() { 
  nop
  nop
  rts 
}

It does not do integer promotion and explicit is casting required.  Defaults to unsigned arithmetic.  It has structs, unions, user-defined type synonyms and aliases, which are all strongly type-checked.  That's really wonderful.  The syntax is pretty friendly for the kinds of operations you need to do, with suffixes for extracting bytes out of words (16 bit) as well as 32 bit values, for extracting pointers, and so on.

 

There is no support for recursion and it generally avoids using the stack.  That's great because it's the only way you can cram a compiled language into only 128 bytes of general purpose RAM.  It does well with ROM'd code.  Otherwise it's basically slightly dumbed down C that avoids some of C's less pleasant syntax pitfalls (IMHO).

 

I'll write more about my experience so far if there's interest, and otherwise I'll probably eventually post my code, but for now it's just a hack unworthy of being called an alpha for a bad demo.

 

Edited by Retrac
  • Like 5
Link to comment
Share on other sites

Interesting... Glancing at the language, it appears to be straddling the line between C and Basic.

 

Probably not for me though.  There's a few things that are off-putting to me... and I'm already knee deep in my own compiler/language development for the Atari 2600.

 

BUT,

I would still be interested in hearing more about your experiences with Millfork though.

Link to comment
Share on other sites

7 hours ago, Retrac said:

Millfork is a C-like language, with adjustments to make it particularly suitable for 8-bit machines, and it targets the 8080/Z80/6809 as well as its best supported target -- the 6502.  It has an optimizing whole-program compiler with some nice tricks that often surprise me at the quality of the code generated.  It is only a couple years old and it is a hobby project, but it has some users with NES games and demos in particular.  The developer is quite friendly and responsive to language-related issues.  There is no 2600-specific support to speak of.  The example program given is all of it at the moment which dos the classic coloured bars and doesn't really demand anything fancy.  (I've done the same thing in C myself, as have others here, I think.)

 

Millfork looks pretty cool with a modular design described for running larger programs than the host system memory, at least with the Commodore 64 documentation I found here where programs > 64K can be written for the C64:

Commodore programming guide - millfork

 

Modularity can allow the VCS to run much larger programs, the new Circus Convey release is modular loading many 4K programs from a 128K ROM.

 

SuperCharger Disk BASIC implements high level language modularity for the VCS with up to 256 6K programs linking to one another and sharing variables. The SuperCharger is a 6K RAM board for the Atari that exchanges the 4K of ROM for 6K of pure user RAM which could make the Millfork featureset more implementable for the VCS.

 

  • Like 1
Link to comment
Share on other sites

3 hours ago, Mr SQL said:

Millfork looks pretty cool with a modular design described for running larger programs than the host system memory

It has basic segment support for bankswitching. You still have to do things like issuing explicit bankswapping calls, but macros are nice for that.  It is possible to implement what I've called fcall() fret() for example, which do a long jump to subroutine from any bank to any bank (pushing return bank and then address) and long return (inverse). 

Quote

Modularity can allow the VCS to run much larger programs [...] SuperCharger Disk BASIC implements high level language modularity for the VCS with up to 256 6K programs linking to one another and sharing variables.

Wow, that's an impressive project!  I like the use of the cassette.  We are almost thinking along parallel lines! 

 

I have only a sketch of a game idea.  I'm not much of a game designer... so for now this is a game engine?  Just scaffolding for other things.  I started with wanting to spread out big calculations over multiple frames.  Now it's more of a general quest to create a nice programming environment for the 2600.  Or to see how much unnecessary abstraction I can fit in 4 KB address space before everything collapses under its own weight :)

 

The first thing I tried was a Forth-like virtual machine (in assembly, unfinished).  It did work.  Elegant really.  Handled all the bankswitching for me.  Multitasking.  Even tried virtualized addresses where 16 bits is a flat address space translated into the bank-switching arrangement.  But it was terribly slow, kind of like I expected.  Probably too slow.  (Though it's not *that* bad since the heavy lifting routines should be in assembly and just called by the VM code).  But the idea of using a virtual machine may still be worth holding on to, just with native, not interpreted, code. 

 

As long as I call my routines yield() (pass control to scheduler) or sleep() (stop running and pass control to scheduler)  at least once in a while (such as a routine's outer loop) then I can largely ignore concerns like "will this calculation finish before overscan is done?".  Just write medium-sized (1 - 2 KB of code) self-contained routines using a common interface (probably something like get a message, do its calculation, write a message, sleep and repeat).  The overhead for that kind of tasking is less than I had worried.  Then I just need to write nice modules that respect a common bankswitching arrangement, and behave cooperatively by yield()ing regularly.  That way the modules can largely ignore VCS-specific details that aren't relevant to them.

 

And all this silliness is because I'm quite prone to writing spaghetti code if I let myself.  Which is fine, even downright necessary, for a 2K or 4K game... but it won't bear out for a big RPG or simulator game with many working parts.

 

6 hours ago, splendidnut said:

I'm already knee deep in my own compiler/language development for the Atari 2600.

I know how you feel!  I'm sinking quickly though.  :) 

Edited by Retrac
  • Like 1
Link to comment
Share on other sites

2 hours ago, Retrac said:

Wow, that's an impressive project!  I like the use of the cassette.  We are almost thinking along parallel lines! 

 

I have only a sketch of a game idea.  I'm not much of a game designer... so for now this is a game engine?  Just scaffolding for other things.  I started with wanting to spread out big calculations over multiple frames.  Now it's more of a general quest to create a nice programming environment for the 2600.  Or to see how much unnecessary abstraction I can fit in 4 KB address space before everything collapses under its own weight :)

 

The first thing I tried was a Forth-like virtual machine (in assembly, unfinished).  It did work.  Elegant really.  Handled all the bankswitching for me.  Multitasking.  Even tried virtualized addresses where 16 bits is a flat address space translated into the bank-switching arrangement.  But it was terribly slow, kind of like I expected.  Probably too slow.  (Though it's not *that* bad since the heavy lifting routines should be in assembly and just called by the VM code).  But the idea of using a virtual machine may still be worth holding on to, just with native, not interpreted, code. 

 

As long as I call my routines yield() (pass control to scheduler) or sleep() (stop running and pass control to scheduler)  at least once in a while (such as a routine's outer loop) then I can largely ignore concerns like "will this calculation finish before overscan is done?".  Just write medium-sized (1 - 2 KB of code) self-contained routines using a common interface (probably something like get a message, do its calculation, write a message, sleep and repeat).  The overhead for that kind of tasking is less than I had worried.  Then I just need to write nice modules that respect a common bankswitching arrangement, and behave cooperatively by yield()ing regularly.  That way the modules can largely ignore VCS-specific details that aren't relevant to them.

 

And all this silliness is because I'm quite prone to writing spaghetti code if I let myself.  Which is fine, even downright necessary, for a 2K or 4K game... but it won't bear out for a big RPG or simulator game with many working parts.

 

Very cool, Millfork's modularity looks like it could be ideal for developing RPG's.

The SuperCharger multiload BIOS might be a good fit for Millfork in this regard.

Dragon Stomper is one example of an Assembly language RPG written for the SuperCharger using multiple 6K modules.

 

looking forward to seeing Millfork develop, the Forth implementation sounds interesting too!

 

  • Like 1
Link to comment
Share on other sites

6 hours ago, Mr SQL said:

The SuperCharger multiload BIOS might be a good fit for Millfork in this regard.  Dragon Stomper is one example of an Assembly language RPG written for the SuperCharger using multiple 6K modules.

I had to read up a bit more on the SC. Some glue code would be necessary for handling the bank-switching and SC ROM routines to read them in.  Otherwise I don't see why it shouldn't work similar to the C64 in that regard.  (Haven't tried that myself).  I think one would just need to load the binary image into a target address, and jump to it.  I'm not used to thinking of the 2600 as having streaming input like that.  It's a little hard to wrap the head around!

 

Edited by Retrac
Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

Loading...
  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...