I switched efforts for now from a C compiler to a much simpler FORTH compiler, and to test it out I made Snake (How original of me right?)

Anyways, here's a screenshot


Currently it (the compiler) isn't in a state that's really useable to people other than me, but I hope to be fixing that in the next 1-2 weeks by breaking it out into its own project, cleaning up the repo, adding documentation, making it controllable with command line arguments, maybe making some pre-compiled binaries.
I'm also planning to add more optimizations to it.

However in the mean time if you think you can get it to work on your own, head on over to https://github.com/unknownloner/calccomp/
run ./compile.sh to compile the code, and run ./calccomp to compile "SNAKE.forth" to "FSNAKE.8xp", edit Main.hs to change the input/output files.

Also here's the code. You may notice I changed the way word definitions are done from standard FORTH, as well I have 'RECURSE' which jumps to the top of the word, or to the 'RECURSEPOINT' in the word if it exists, but I don't have any other form of looping right now.

In understanding the ASM words, know that the only thing on the stack when they run is the program data, no return addresses.

Code:

( VX and VY are velocity )
VAR VX
VAR VY

VAR HEADX
VAR HEADY

VAR HEAD_OFF
VAR TAIL_OFF

VAR BSIZE


1 CPU_SPEED (15mhz)
0 0 0x0000 320 240 FILLRECT
0 0 0xFFFF 228 228 FILLRECT
RUNGAME
0 CPU_SPEED (6mhz)

( Runs the game loop multiple times, return val determines if it runs again )
WORD RUNGAME {
    ( Initialize variables )
    4 HEADX !    0 HEADY !
    1 VX !       0 VY !
    4 HEAD_OFF ! 0 TAIL_OFF !
    5 BSIZE !
    2 2 0x0000 224 224 FILLRECT
    CLEARBOARD
    5 INITSNAKE
    GENFOOD
    GAMELOOP IF
        RECURSE
    THEN
}

( Actual loop for in the game )
( Return TRUE if game should restart, FALSE if it should exit )
WORD GAMELOOP {
    (Remember, TRUE = -1, FALSE = 0)
    GETCSC
    DUP 15 = IF
        DROP
        FALSE RETURN
    THEN
    DUP 1- 4 U< IF  (If key code is from 1-4...)
        DUP DUP DUP (STACK: KEY KEY KEY KEY)
        (Y axis)
        4 = SWAP 1 = -      (-1 if key up, 1 if key down, 0 if both)    (STACK: KEY KEY VELY)
        DUP VY @ AND 0= IF (Same as VX for VY)
            VY !
        ELSE DROP THEN

        (X Axis)
        2 = SWAP 3 = -      (-1 if key left, 1 if key right, 0 if both) (STACK: VELX)
        DUP VX @ AND 0= IF  (If VX & NEWVX == 0, set VX to NEWVX, else DROP it)
            VX !
        ELSE DROP THEN
    ELSE DROP THEN

    (Increment head offset)
    HEAD_OFF @ 1+ 1023 AND
    HEAD_OFF !

    (Move head)
    HEADX @ VX @ + 31 AND
    DUP HEADX !
    HEAD_OFF @ BODY_X + c!

    HEADY @ VY @ + 31 AND
    DUP HEADY !
    HEAD_OFF @ BODY_Y + c!


    (Food/collision test)
    HEADX @ HEADY @
    CELLADDR c@ (Tile at new head loc)
    DUP 1 = IF
        DROP
        TRUE RETURN (Restart game)
    THEN
    2 = IF
        GENFOOD
    ELSE
        (Erase/move tail)
        TAIL_OFF @ DUP DUP
        BODY_X + c@ SWAP
        BODY_Y + c@
        2DUP 0 SETCELL
        0x0000 FILLCELL
        1+ 1023 AND
        TAIL_OFF !
    THEN

    (Draw head)
    HEADX @ HEADY @
    2DUP 1 SETCELL
    0x07E0 FILLCELL

    245 SLEEP
    RECURSE
}

(
Initializes the snake on LCD/board
Input: Length
)
WORD INITSNAKE {
    1-
    DUP DUP BODY_X + c!        (Set X position in body data)
        DUP BODY_Y + 0 SWAP c! (Set Y position in body data)
    DUP 0 0x07E0 FILLCELL
    DUP 0 1 SETCELL
    DUP 0!= IF
        RECURSE
    THEN
    DROP
}

(Generates a new food tile)
WORD GENFOOD {
    RAND RAND (X and Y)
    2DUP CELLADDR c@ IF
        2DROP
        RECURSE          (Jump back to start until we find a blank tile)
    THEN
    (2DUP FOODY ! FOODX ! (Save food position))
    2DUP 2 SETCELL       (Set food position on board)
    0xF800 FILLCELL      (Draw food)
}

( Input: X Y TYPE )
WORD SETCELL {
    -ROT CELLADDR c!
}

( Gets cell addr for cell X Y )
WORD CELLADDR {
    32 * +      ( Y * 32 + x)
    BOARD +     ( Add board address )
}

( Fills board with zeroes )
( Input: Addr Val Amount )
WORD CLEARBOARD {
    BOARD >R (addr on R stack)
    1024     (Amount of bytes to fill)
    RECURSEPOINT
    2-       (We fill 2 at a time)
    DUP R@ + 0 SWAP !
    ?DUP IF  (If offset > 0, recurse)
        RECURSE
    THEN
    R> DROP (Drop BOARD)
}

( Fills a board cell )
( Input: X Y COLOR )
WORD FILLCELL {
    ROT
    7 * 2+  (X = X * 7 + 2)
    ROT
    7 * 2+  (Y = Y * 7 + 2)
    ROT 7 7 (Stack: X Y COLOR 7 7)
    FILLRECT
}

( INPUT: X Y COLOR WIDTH HEIGHT )
WORD FILLRECT {
    ROT >R                (Color on R stack)
    2SWAP                 (STACK: WIDTH HEIGHT X Y)

    DUP DUP               (W H X Y Y Y)
    0x50 SETLCD           (Win top)
    0x20 SETLCD           (Cursor y)
    2 PICK + 1- 0x51 SETLCD  (Win bottom, STACK: WIDTH HEIGHT X)

    DUP DUP               (W H X X X)
    0x52 SETLCD           (Win left)
    0x21 SETLCD           (Cursor x)
    2 PICK + 1- 0x53 SETLCD  (Win right,  STACK: WIDTH HEIGHT)

    R>                    (Color off R stack)
    PIXELFILL
}

ASMWORD BODY_X {
    .var 1024, snakeBodyX
    ld hl,snakeBodyX
    push hl
}

ASMWORD BODY_Y {
    .var 1024, snakeBodyY
    ld hl,snakeBodyY
    push hl
}

ASMWORD BOARD {
    .var 1024, board
    ld hl, board
    push hl
}

ASMWORD CPU_SPEED {
    pop hl
    ld a,l
    out (20h),a
}

ASMWORD SLEEP {
    pop hl
WasteTime:
    djnz WasteTime
    dec hl
    ld a,h
    or l
    jr nz,WasteTime
}

ASMWORD GETCSC {
    b_call(_GetCSC)
    ld h,0
    ld l,a
    push hl
}

( Generate a random number from 0 - 31 )
ASMWORD RAND {
    .var 2, randData
    ld hl,(randData)
    ld a,r
    ld d,a
    ld e,(hl)
    add hl,de
    add a,l
    xor h
    and 31
    ld (randData),hl
    ld h,0
    ld l,a
    push hl
}


( Sets an LCD register )
( INPUT: VAL REG )
ASMWORD SETLCD {
    pop hl
    ld a,l
    out (10h),a
    out (10h),a
    pop hl
    ld a,h
    out (11h),a
    ld a,l
    out (11h),a
}

( Fills WIDTH * HEIGHT pixels with COLOR )
( INPUT: WIDTH HEIGHT COLOR )
ASMWORD PIXELFILL {
    pop de ;Color
    pop bc ;C = height
    pop hl ;HL = width

    ld a,22h
    out (10h),a
    out (10h),a

    ld b,c ;height
    ld c,11h ;Output reg

_pixelFillLpX:
    push bc ;Save height
    _pixelFillLpY:
        out (c),d
        out (c),e
        djnz _pixelFillLpY
    pop bc
    dec hl
    ld a,h
    or l
    jr nz, _pixelFillLpX
}


And if you just want to test the program, download from
http://z80.unknownloner.com/FSNAKE.8xp
No DoorsCSE required.
Amazing work, i love the speed!
Working on languages that are simpler than C for your first compiler is absolutely the correct decision. Are you doing something sensible with tail-calls, or are recursive loops going to blow out the stack? Or does "recurse" not touch the stack at all and really just is being used for jumps?
elfprince13 wrote:
Working on languages that are simpler than C for your first compiler is absolutely the correct decision. Are you doing something sensible with tail-calls, or are recursive loops going to blow out the stack? Or does "recurse" not touch the stack at all and really just is being used for jumps?


Every routine (except those written in assembly) have 2 labels, one for the very start of the routine which takes care of the call stack, and one right after. Here's an example:

Code:

CTRL_19:
POP HL
DEC IX
LD (IX), H
DEC IX
LD (IX), L
RWDEF_CTRL_19:


If the "RECURSEPOINT" word appears anywhere in the definition, the recurse label is put there instead, like so:

Code:

WORD COUNT_TO_5 {
    0
    RECURSEPOINT
    1+
    DUP 5 != IF
        RECURSE
    THEN
}


"RECURSE" just compiles down to an unconditional jump to that label.

Although, on the CSE the FORTH return stack is a page of ram at 0x4000-0x7FFF, so really you could call a word within itself around 8000 times with no worries other than running into your variables eventually Razz. Obviously though RECURSE is the faster and better option.

Also, now that I'm thinking about it, it would be pretty trivial to implement a conditional RECURSE (and useful too), so I think I'm going to do that. IFRECURSE maybe.
I'm impressed that you've made a third-party compiler that can actually compile code into useful programs for a TI calculator. That has long been the major sticking point of porting other languages to graphing calculators (the uttering of which causes me to glare at Antelope and its perpetually-mutating grammar). I'm suitably wowed by your demo, of course, and I look forward to continuing progress on this project.
I'm going to take a moment here and explain how the ASM code generation works for anyone interested.

Before beginning, if you aren't aware, FORTH 'words' are like routines or functions. They're a block of code which can be called from somewhere else. Also, the STDLIB consists of all of the pre-defined operations that FORTH programs can build off of. Most of them are standard FORTH operations, although some are conveniences I've added, like "DI" and "EI", so you don't need to write an ASM word just to toggle interrupts.

Skip down to the bold text if you only care about interop with other assemblers.

So, I've written an assembler which has 3 stages
Preprocess -> Parse -> Assemble

The interesting part is that haskell lets me write 'quasiquoters' which can parse arbitrary code at compile-time of the program, and output expressions (or data). I've created a quasiquoter which I've used to great effect which parses z80 assembly and outputs the resulting list of ASM expression data. This has 2 advantages.
1. I can get syntax errors from my assembly when compiling
2. I can easily include the result of arbitrary haskell code in the ASM.

For example: I might write this to load a value or label's address into HL

Code:

[asm|ld hl,@{someVariable}|]


This makes code generation simple as writing inline assembly for the pre-defined words I want. If you'ld like to see the details, go ahead and read the code (although be warned, it's a bit of a mess right now IMO. I'm going to be cleaning it up), but the gist of it is that the compiler goes through and compiles all FORTH expressions like so:

Numeric constants are simple:

Code:
ld hl, X \ push hl

String constants are similar. No matter how many times a string occurs, it's data is only output once. A string expression compiles to

Code:
ld hl,PTR_TO_STR \ push hl

A few FORTH words (IF, ELSE, THEN, RECURSE, RECURSEPOINT) are special cased to check the current state for things they need
At this point, we're left parsing an arbitrary token. If it matches any STDLIB words, code will be generated for that word (or calling it for non-inline STDLIB words). Otherwise, if it matches any defined variables, it compiles to

Code:
ld hl,PTR_TO_VAR \ push hl.

Finally, if it matches none of the preceding conditions, it will compile to calling the word named by that token. It doesn't actually check if that word has been defined, so right now if you try to use a word without defining it you will get a very cryptic error message like 'Unknown label: CTRL_13', which I'm hoping to resolve.

All ASM outside of words are output sequentially at the start of the program, and then all word definitions are output after it. Non-inline STDLIB word definitions come last, although luckily only the ones you actually use have code generated.
The resulting code structure is
VAR_DEFS
CODE_PRE (setup stack, ram pages)
CODE
CODE_POST (restore stack, ram pages to previous state)
WORD_DEFS
STRING CONSTANTS

After the assembly is generated, there are a few optimization passes which occur. The primary and most important ones are:
Occurrences of "push hl \ pop hl" are removed entirely
Occurrences of "push hl \ pop de" become "ld d, h \ ld e, l" and vice-versa.
This covers most of the low hanging fruit as far as optimizations go, though I have some more complex ones planned. Right now these optimizations are applied to ALL the ASM code, not just generated FORTH code, so it affects inline ASM too. However, I'm going to be switching it to only passing over FORTH code, that way people writing ASM know that what they write is what they get, and also it lets me do optimizations based on known behaviour of the generated code, like turning "push hl \ pop de" into "ex de,hl"

Interop with other assemblers
Since the parsing step is separate from the assemble step, it has no limits to what directives it can parse. Also, it knows how to print itself back into a string representation. This means that if, when writing inline assembly for FORTH words, you want to use features of other assemblers like BRASS, you can, just by writing the output assembly to a file and assembling it yourself.

There are a few limitations to that however.
The first is one which I believe I can easily fix: My parser doesn't allow defining of labels with '@', and it doesn't handle relative label jump syntax from brass, i.e.

Code:

jp {2@}

The second is one which would take more work to fix and currently limits you to using my assembler or brass: I rely on .varloc and .var in the FORTH compiler for variable allocation, so if you use an assembler which doesn't support that you're out of luck.
Unknownloner wrote:

"RECURSE" just compiles down to an unconditional jump to that label.

This is useful. It's also not recursion, it's just a goto. Please don't call it recursion, because that implies things you're not doing (like stack frames).
elfprince13 wrote:
Unknownloner wrote:

"RECURSE" just compiles down to an unconditional jump to that label.

This is useful. It's also not recursion, it's just a goto. Please don't call it recursion, because that implies things you're not doing (like stack frames).


Suggest a better name and I'll use it

Edit: Thoughts on 'RESTART' or 'AGAIN'?

EDIT2:
Take this with a grain of salt as I just woke up and haven't eaten yet, but...

So I'm thinking of changing RECURSE to actually do proper recursion, so if it's the last piece of code in a word it'd jump to the start, otherwise it'd either call the start, or give you an error saying that it couldn't optimize it to a jump (compiler flag for if you only want tail-call optimizable code).

Then, take the current function of RECURSE and rename it, since it is useful to be able to restart anywhere in the word, (see my food gen code). Honestly right now I'm leaning towards RESTART.

Not going to add full-blown GOTOs though since those can cause all manner of problems Razz.

As for when RECURSE could be optimized, there's really 2 scenarios:
1. It's the last word in a word definition
2. It's the second to last word, and the last word is THEN.
So that's pretty simple.

I do think that having a definable RECURSEPOINT be a thing is useful, since that way someone that wants to set up some initial conditions before recursively solving a problem doesn't need the overhead of two separate word definitions. When adding RESTART, I'm not sure if I should add a separate RESTARTPOINT for it, or just have it jump to the RECURSEPOINT. Opinions on that would be appreciated.

. . .

On a different subject, would it be useful to define a word as INLINE? It should be simple to implement, but for anything other than simple definitions I feel like it'd be more of a trap, bloating people's program sizes for not a whole lot of speed gain. I could see it being a little more useful to define an ASMWORD as inline though.
  
Register to Join the Conversation
Have your own thoughts to add to this or any other topic? Want to ask a question, offer a suggestion, share your own programs and projects, upload a file to the file archives, get help with calculator and computer programming, or simply chat with like-minded coders and tech and calculator enthusiasts via the site-wide AJAX SAX widget? Registration for a free Cemetech account only takes a minute.

» Go to Registration page
Page 1 of 1
» All times are UTC - 5 Hours
 
You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum
You cannot vote in polls in this forum

 

Advertisement