Skip to content

ForthField examples

ForthField is Field's Forth: a data stack and a dictionary of words that push and pop it. A whole definition can fit on one line (: sqr dup * ;), and you can rewrite the dictionary while the box is running. There are words for control flow, vec2 helpers and FLine drawing, plus a reflection bridge for calling methods on JVM objects.

To get a ForthField box, run Set Main Language: ForthField on a parent box; its children then run ForthField rather than Kotlin. Every example below is ForthField you can paste into such a box.

A tiny stack language

Every word transforms the stack. Read stack comments from left to right and the code becomes much easier to follow.

:::static
: sqr ( n -- n2 ) dup * ;
: hypot2 ( x y -- d2 ) sqr swap sqr + ;

s"hypot^2 = " 3 4 hypot2 str str+ println

one output panel labelled stdout, holding a single line of green monospace on black: hypot^2 = 25.0

: sqr ... ; defines a new word. dup copies the top stack item, * multiplies the top two numbers, and swap flips the top pair so hypot2 can reuse sqr on both inputs.

What each line left on the stack

The editor keeps a record of what each line did the last time the box ran. Rest the cursor on a line and the note beside it draws the data stack as that line left it, top uppermost, and names the words the line defined; a run that failed marks the line it stopped on and the lines it never reached. The note reads that record — resting on a line never runs it. The stack outlives a run, so a box that wants a fresh one starts with clear. See Line notes.

A run that finishes cleanly and prints nothing says what it left behind, rather than a bare ok: the depth of the stack and its top values, the topmost in bold. An empty stack gets a plain ok. Both are in this figure — the box has just been run, the strip under its last line is what the whole run left, and the note is what line 6, sq, left, with sq having squared the 10 above it:

A seven-line ForthField box in the editor. Under its last line, s-quote done, a strip reads ok · <3> 3.0 100.0 done, with done in bold: what the whole run left. The caret rests on line 6, sq, and the note below draws the stack that line left as two cells, top uppermost and outlined: 100.0 above 3.0. Under the picture, in small muted type: line 6 · last run, just now.

Variables, strings, and output channels

ForthField variables are named storage cells. They are enough for counters, state machines, and launch-time animation state.

:::static
variable count
0 count !

1 count +!
2 count +!
3 count +!

s"ForthField writes straight into a Field output area." display
s"count = " count @ str str+ s"debug" display-channel

Two output panels: the one labelled output holds the sentence ForthField writes straight into a Field output area., and the one labelled debug holds count = 6.0

display writes to the default output channel. display-channel names the disclosure panel explicitly, which is enough to keep pretty output and debug output apart.

Building an FLine directly

This is the smallest complete drawing workflow: make an FLine, add segments, style it, and leave it on the stack.

:::static
: stroke-ribbon ( -- f )
  fline
  0   0   moveto
  120 20  lineto
  160 90  210 90  260 20 cubicto
  320 0   lineto
  true  fline-stroked
  false fline-filled
  1.0 0.55 0.2 1.0 fline-color
  2.5 fline-thickness ;

stroke-ribbon

an output panel labelled lines holding one thick orange stroke on black. It falls in a shallow straight run from the upper left, turns at a corner about a third of the way across into a smooth curve down to a rounded minimum just past centre, then rises and turns at a second corner into a straight climb to the upper right

This example is close to SVG path construction. moveto, lineto, and cubicto build the geometry. The later fline-* words mutate style properties on the same object and push it back so the chain can continue.

Vec2 helpers: geometry that arrives as points

The vector helpers are useful when geometry is already arriving as points rather than loose coordinates.

:::static
: zig ( -- f )
  fline
  0    0 vec2 fline-moveto-v2
  40  20 vec2 fline-lineto-v2
  80 -10 vec2 fline-lineto-v2
  120 25 vec2 fline-lineto-v2
  160 -5 vec2 fline-lineto-v2
  true  fline-stroked
  false fline-filled
  0.2 0.85 1.0 1.0 fline-color
  4.0 fline-thickness
  1.0 0.4 0.2 1.0 fline-node-color ;

zig

an output panel labelled lines holding a bright cyan zigzag of four straight segments on black: down from the upper left to a corner, up to the highest point near the middle, down to the lowest point, and up again to finish at the right edge at about the height it started

fline-node-color styles the current node rather than the whole line. Use it to mark a control point, an endpoint, or the last place a computation landed.

Reflection: reaching a JVM class by name

The reflection bridge resolves a class by name, constructs it, and calls methods without leaving ForthField, which is how ForthField reaches anything the fline-* words do not already cover.

:::static
variable f
s"FLine" class-for @? newInstance f !

f @ 0   0   0   3 @?! moveTo drop
f @ 120 30  0   3 @?! lineTo drop
f @ 220 10  0   3 @?! lineTo drop
f @ 260 80  0  320 80  0  380 20  0  9 @?! cubicTo drop

f @ @? properties s"thickness" 3.0 2 @?! put
f @ @? properties s"color" 0.95 0.95 1.0 1.0 vec4 2 @?! put

f @

an output panel labelled lines holding one white stroke on black. It falls gently from the upper left to a shallow corner a quarter of the way across, rises gently to a second corner just past centre, then curves down to a rounded minimum and sweeps back up to the right edge

@? guesses what you mean. It looks for a public field of that name first, then for a method, preferring one with no arguments and otherwise taking the first whose argument count fits what is on the stack. So it can pick a different overload depending on how deep the stack is. A field holding null pushes nothing, which leaves the stack one shorter than you might expect. On a Class, new and newInstance construct an instance. When @? picks the wrong overload, use @?!, which takes the object and then the argument count from the top of the stack.

For styling, numeric and vector values are a good fit for reflected properties.put(...). Boolean style toggles are better expressed with the dedicated fline-* words.

The class helpers are useful on their own:

s"FLine" class-for @? getName println
s"Vec3" class-for @? getName println
s"Fli" class-matches println

class-for takes a fully-qualified or a simple class name. If a simple name matches more than one class, the error lists candidates to choose from. class-matches takes a prefix and returns the fully-qualified names that start with it, which is how you find what a class is called; class-help prints a shorter version of the same list.

Dictionary tools, recursion, and temporary stack storage

words shows the dictionary, see name prints a definition, and forget name removes a user-defined word when an experiment has gone stale.

: sqr dup * ;
words
see sqr
forget sqr

recurse means "run the word I am currently inside." Here it makes a small factorial. The base case leaves 1; the recursive case keeps a copy of n, computes (n-1)!, then multiplies.

: fact ( n -- n! )
  dup 1 <= if
    drop 1
  else
    dup 1- recurse *
  then ;

5 fact println

The return stack is a second, temporary stack. Use it sparingly, mostly to park a value while the data stack is doing something else. >r moves to the return stack, r@ copies back, and r> moves back.

: add-twice ( a b -- a+b+b )
  >r        \ save b away from the data stack
  r@ +      \ add a copy of b
  r> + ;    \ restore b and add it again

10 3 add-twice println

again, diagnostics, and display helpers

begin ... again loops forever. In a launched box that is what you want, as long as the loop calls frame to hand each tick back to Field. In a box that runs once, leave the loop with leave.

variable n
0 n !

: count3
  begin
    1 n +!
    n @ 3 >= if leave then
  again ;

count3
n @ println

When something fails, ForthField errors include the current token, a stack snapshot, and the word call stack. For example:

: inner nope ;
: outer 10 inner ;
outer

The three-line ForthField box in Field's editor, with a red error marker naming the unknown word nope, the data stack holding 10, and the call stack inner inside outer.

The error names the unknown word, shows that 10 was still on the data stack, and shows the path through outer into inner.

The small display vocabulary gives ForthField direct access to Field output areas without going through Kotlin:

:::static
s"<b>Hello from raw ForthField HTML.</b>" html

0.72 1.0 s"progress" bar
42 s"answer" watch

1 4 9 16
stack-display
plot-stack

five stacked output panels from one Forth box. output holds the rendered sentence Hello from raw Forth HTML, this time as text rather than tags. progress is a blue bar filled most of the way, labelled 72.0% and 0.72 of 1.0. answer holds answer = 42.0 in bold. stack is a three-column table of depth, value and type: 0 16.0 Double, 1 9.0 Double, 2 4.0 Double, 3 1.0 Double. plot is a small chart of four blue bars climbing left to right

html and html-channel send raw HTML. watch gives a named value its own output channel. stack-display shows the current stack as a table, and plot-stack turns numeric stack values into a quick bar chart.

Extending the dictionary from Kotlin

The host bridge goes the other direction too. Kotlin can inject new words when ForthField should drive a renderer, a buffer, or some other JVM-side subsystem.

:::static
import afield.lang.forth.ForthInterpreter

val forth = ForthInterpreter()

forth.defineWord("append-vec4", "( buf x y z w -- buf )") {
    val w = popNum().toFloat()
    val z = popNum().toFloat()
    val y = popNum().toFloat()
    val x = popNum().toFloat()
    val buf = pop() as DynamicallyWriteable

    buf.append {
        put(Vec4(x, y, z, w))
    }

    push(buf)
}

With a real buffer object on the ForthField stack, the call site stays tiny:

0.0 1.0 0.0 1.0 append-vec4
1.0 0.0 0.0 1.0 append-vec4
0.0 0.0 1.0 1.0 append-vec4

The host keeps hold of the resource; ForthField only names it.

Launch mode and frame

frame animates a launched ForthField box. The word below draws a line that turns a little each tick, and shows the tick count in a debug channel:

variable tick
variable line
0 tick !

: spinner ( -- )
  s"FLine" class-for @? newInstance line !

  line @ 0 0 0 3 @?! moveTo drop

  line @
  tick @ 0.05 * dup cos 120 *          ( x )
             swap sin 120 *            ( x y )
  0 3 @?! lineTo drop

  line @ @? properties s"thickness" 2.0 2 @?! put
  line @ @? properties s"stroked" true 2 @?! put
  line @ s"geom" display-channel

  s"tick=" tick @ str str+ s"debug" display-channel

  1 tick +!
  frame
;

spinner

Above is the box launched for six seconds. The geom panel scales each tick's line to fit, so the turn shows as a changing slope, and the debug panel counts the ticks.

frame hands control back to Field, and the word starts again on the next tick, so keep anything that must last between ticks in variables, not on the stack.

When a definition misbehaves

Use see name to check that the word running is the definition you think it is, and read the error before adding prints: it already names the failing token, the stack and the chain of word calls.

Builtins and your own words share one dictionary, so : dup ... ; replaces the real dup for the rest of the run, with a warning in the output; forget dup brings the builtin back.