Log In  


Cart #japi_jorth_1-5 | 2024-12-09 | Code ▽ | Embed ▽ | License: CC4-BY-NC-SA
3

Jorth

A small stack-based language interpreter implemented in Pico-8, oh and a text editor too. It's a programming toy. If you know Forth, this language won't be too hard to learn.

Controls

Ctrl-G: Switches modes
Ctrl-D: Deletes the current code
Ctrl-C: Copies the code (bugged)
Ctrl-V: Pastes whatever is in the clipboard (bugged)
Ctrl-H: Replaces the current content of the program with a help menu
Click to run the code.

Changelog

0.1:

  • First version!

0.2:

  • Added Ctrl-H for docs
  • Added STOP intrinsic

0.3:

  • New intrinsics for working with Lua
  • New docs for IF-ELSE, DEF, and Lua intrinsics
  • Smarter error messages

0.4:

  • @CALL_LIMIT meta intrinsic added, and docs for it
  • LET bindings!
3


1

Be nice if there was a documentation


1

Also agree with @SwordF , I'm struggling.

strings viewed as text with dump show as 0, but "TXTB" as number with . displays 0.0002 ???

How about adding a new LUA keyword ?

10 30 20
3 "mid" LUA
.

Would display 20
the parameters of the LUA call would be the number of stack elements to pop and pass to the lua function, and the function name. The return value(s) of the call would be put on the stack.
Example :

def cosinus
1 "cos" LUA
end
0.5 cosinus . # -1


@SwordF I added some documentation, you can use Ctrl-H to see it.

@RealShadowCaster A LUA keyword would be pretty nice! But, it would be hard to implement. I would have to know the number of arguments a Lua function takes to know how many arguments to pop off the stack, but as far as I know there is no way to get this information. I will try making a global table with the function name, function itself, and number of arguments, but this would only work on the functions I add to the table.


1

@Jaypi
I don't think the number of parameters is the main problem for the LUA keyword :
It would take two parameters : the number of elements of the stack to consume as parameters and the function name.
You can check if the function call is valid by checking the type of _ENV[function_name] and then call it with the popped parameters.
Functions like poke that naturally have N arguments to write to memory will need the number of parameters anyway.

What I don't see is how to pass booleans or arrays to function calls for example.

Let's say you want to set color 3 to opaque.
palt(3,false)

you could try

3 #color 3
1 0 = # false
2 "palt" LUA #call palt with 2 parameters

, but
1 0 =
returns 0 in JORTH, and 0 is true in Lua, so the transparency of color 3 would be set to true...


@RealShadowCaster Ah, I see now. I was confused about the keyword at first. About the Boolean and array problem, I think I have a simple fix.

I could add intrinsics that take a value, and transform it into a Lua value. This would work on booleans and strings. Nil could have its own intrinsic. If you try to operate on these values normally, you’ll get an error.

For arrays where the keys are numbers, you could call the PACK function from Lua if the arguments are on the stack.

Edit: It’s done!


1

Hey, I tried to program the display of my logo in JORTH, and failed miserably.

original plan was to have X Y direction fractal_level on the stack as parameters.
I couldn't find a way to access X (the 4th element in the stack)
so I changed to 128Y+X D L
LUA calls seem to use parameters backwards :
10 64 "pset" 2 LUA
draws in X=64 and Y=10

when there's infinite recursion, you get an OUT OF MEMORY crash and don't get any of the output...

Here's the buggy WIP code

# display RealShadowCaster's logo in JORTH
# stack : x y direction (0123=enws) level (0 is nothing drawn)
# can't grab 4th element
# stack 128*y+x direction level

def fractleft
#"l" puts
dup 0 = if
else
1 -
tl fractright fw
tr fractleft fw
fractleft tr fw
fractright tl
1 +
end
end

def fractright
dup 0 = if
else
1 -
tr fractleft fw
tl fractright fw
fractright tl fw
fractleft tr
1 +
end
end

# turn left
def tl
"l" puts
swap
1 +
dup 4 = if
drop 0
end
swap
end
#trun right
def tr
"r" puts
swap
1 -
dup 0 < if
drop 3
end
swap
end
#forward (2 pixels)
def fw
"f" puts
swap
dup 0 = if #x=x+2
rot 2 + 
rot rot 
end
dup 1 = if #y=y-2
rot 256 -
rot rot swap
end
dup 2 = if #x=x-2
rot 2 -
rot rot swap
end
dup 3 = if # y=y+2
rot 256 +
rot rot swap
end
rot dup
dup 127 "band" 2 lua #x
"x=" puts dup .
swap 128 / #y
"y=" puts dup .
"line" 2 lua
dup .
10 emit
rot rot
end

"cls" 0 lua
10 64 "pset" 2 lua dump
10 64 128 * + 0 2 fractleft
10 emit
dump

"flip" 0 lua
"flip" 0 lua
"flip" 0 lua
"flip" 0 lua
"flip" 0 lua
"flip" 0 lua
"flip" 0 lua
"flip" 0 lua
"flip" 0 lua
"flip" 0 lua
"flip" 0 lua
"flip" 0 lua
"flip" 0 lua
"flip" 0 lua
"flip" 0 lua
"flip" 0 lua
"flip" 0 lua
"flip" 0 lua
"flip" 0 lua
"flip" 0 lua
"flip" 0 lua
"flip" 0 lua
"flip" 0 lua
"flip" 0 lua
"flip" 0 lua
"flip" 0 lua
"flip" 0 lua
"flip" 0 lua
"flip" 0 lua
"flip" 0 lua
"flip" 0 lua
"flip" 0 lua

@RealShadowCaster Wow! That's really cool! To address the problems you have:

The recursion thing is something I wanted to fix. I'm going to add a function call limit. If you exceed the limit, the program will crash. This error is very similar to stack overflow errors in other languages.

For not being able to access arguments on the stack, I've been thinking about maybe adding an intrinsic for accessing specific arguments on the stack, but I think I'm going to add an entire new language feature. I've seen this used in a language called Porth. The LET keyword will introduce a new binding, you can use it like this:

1 2 3
let c b a in
a . b . c . a . # 1231
end

dump # []

It basically adds variables to the language.


I've looked at FORTH documentation, and there's a lot of ways :
2SWAP swaps the top two pairs of the stack.
There's also PICK that duplicates the Nth element of the stack, so
0 PICK does the same as DUP
1 PICK does the same as OVER
What I was after was 3 PICK to duplicate the 4th element of the stack. (X in my example.

During my studies, there was a rivalry where half the class was using the TI-85 (me included) that was programmed with TI-BASIC, and the other half the HP48G or HP48GX witch was more powerful but was programmed with a less accessible language that was a sort of extended FORTH. One of the keywords was STO.
2 'x' STO would store value 2 in the variable named x .
You could retrieve the value with RCL if I remember correctly :
'x' RCL

All these variables were permanent and tended to pile up, storage was in short supply, and the calculator owner was responsible for manual garbage collection...
The TI85 was no better in that regard. Since there was no keyword to delete variables in TI-BASIC, the best you could programmatically do was assign an empty string to your big array at the end the program... Worse yet, the TI85 namespace was global, and programs were protected from deletion by re-assignation, so if you borrowed a friend's calculator and created a program named I, pretty much every previously working program would start to choke with cryptic error messages when trying to use a for loop...
HP48 trolling was also rampant, the two most memorable trolls were a game that would silently litter the memory with random folders and variables, and another one that had a fake "turn off" option that cleared every pixel of the screen and turned on the infra-red emitter while keeping the infrared indicator off, then stayed active in an infinite loop, leading to empty batteries in a couple hours time... This was deemed "OK", despite the real losses in time and battery money. The only two agreed no-nos were deleting other people's stuff and irreversible hardware damage. Strange times...


@RealShadowCaster PICK was definitely in my mind when I was implementing LET. (I'll probably add it in v0.5, 2SWAP as well). It's just, it's way easier to think about things in LET bindings for me than manipulating the stack. So, I went for this one first.

I think I've heard of a stack-based programming language for a calculator, but I never knew what it actually was. A STO instruction is interesting, but for where I'm heading with the language, it doesn't really make sense here, so I'll put that off the table. I'll look into its other features.

I've tried programming in TI-BASIC before, and know that you have to zero out all the variables you'll use in your program before using them. This problem comes directly from the variables being shared between programs, I think. This is a pretty big problem in my eyes, and it's kinda a chore. The variables in this case are more like memory slots instead of variables. Memory slots sounds like a good idea to me, but not for use in variables. Maybe I should explore that more.


@Jaypi, tried to implement PICK with LUA calls.
Unfortunately, it seems you only ever get the 1st return value of a call.
For example, cursor() returns three values (X,Y and color) but only the X value is returned on the JORTH stack. No way to use unpack() to retrieve a modified array in its entirety for example.(I wanted to implement PICK with LUA calls to pack(), deli(), add() and unpack() )

I also think handling the parameters of LUA calls should be in the opposite order:
v1 v2 -
puts v1-v2 on the stack
but if you do
v1 v2 "shl" 2 LUA
you get v2<<v1 on the stack instead of v1<<v2 that would be in line with the minus function.


@Jaypi
PICK in JORTH

Fixed with let ... in statements

# display RealShadowCaster's logo in JORTH
# stack : x y direction (0123=enws) level (0 is nothing drawn)

20000 @call_limit

def fractleft
dup 0 = if
else
1 -
tl fractright fw
tr fractleft fw
fractleft tr fw
fractright tl
1 +
end
end

def fractright
dup 0 = if
else
1 -
tr fractleft fw
tl fractright fw
fractright tl fw
fractleft tr
1 +
end
end

# turn left
def tl
swap
1 +
dup 4 = if
drop 0
end
swap
end
#trun right
def tr
swap
1 -
dup 0 < if
drop 3
end
swap
end

#forward (2 pixels)
def fw
let level direction y x in
 direction 0 = if #x=x+2
 x 2 + y
 end
 direction 1 = if #y=y-2
 x y 2 - 
 end
 direction 2 = if #x=x-2
 x 2 - y 
 end
 direction 3 = if # y=y+2
 x y 2 + 
 end
 let ny nx in
 ny nx y x "line" 4 lua
 nx ny direction level
 end
end
end

def frame_wait
  dup 0 > if
   "flip" 0 lua
   1 - frame_wait
  else
   drop
  end
end

def wait
  30 * frame_wait
end

"cls" 0 lua
12 "color" 1 lua
dump
0 127 0 6 fractleft
dump
1 wait
"screen" luastring "extcmd" 1 lua

@Jaypi
I'm thinking about writing games in Jorth. But I'd need a few extra functionalities to make it a viable.

  • escape key to interrupt running programs
  • auto run if a Jorth program is found in high memory or in webpage url
  • finer controls on call overflows (your game loop is infinite and shouldn't count for example)
  • maybe special treatement if JORTH functions are named _init _draw and _update as an alternative way to implement the game loop ? Might be be tricky
    with a global stack... Might be interesting to see how old 8bit systems handled interrupts in FORTH. Maybe interrupt functions worked on separate stacks and needed to peek their data from memory ? Or maybe a global stack and the interrupt just shoudln't use what's in in and return wothout leavin any new data in it so the caller resumes without problem ?


[Please log in to post a comment]