Requirements

A StackTalk installation, see here

What follows is a bunch of code you can run, it is assumed that you are familiar with sk and its various flags, the --help flag (aka sk --help) has the basics, and right now there isn't much more.

Basic Values

StackTalk is built out of a few different types of values

64 bit signed integers:

1 2 +
3

64 bit floating point numbers, commonly known as doubles:

3.4 2.5 *
8.5

Strings, which are currently immutable buffers of bytes. There are two syntaxes for strings. The first is ending a word with a :, the second is using quotes "like so".

foo: " bar" +
"foo bar"

Booleans, which can be true or false, and have the typical operations on hand:

true false or? true and? not?
false

Complex values, part 1

Quotations are how StackTalk bundles up code to be executed at a later point. They start with [ and end with ]

[ 1 2 + ]
quote[ [ 1 2 + ] ]

If you want to execute a quotation, the &run word will do that

[ 1 2 + ] &run
3

Objects are StackTalk's core composite data structure. They are built out of collections of named stacks. You can create a new one with [ ] obj, and you can push to a stack in object using >stackName.

[ 1 >x 2 >y ] obj
obj[ fields: x y ]

StackTalk's Core Stacks

StackTalk has 3 main stacks that it works with:

These stacks are independent of a given object (but are rolled up into coroutines, we'll talk more about that later).

The data stack

The data stack is where values hang out, where literals push data to, and where operations pull data from, by default. If you've used Factor or Forth, you're familiar with this one, and swap and dup do the expected things. Because of objects being built out of multiple named stacks, StackTalk usually uses less stack shuffling words.

The subject/me stack

The subject stack is how StackTalk knows what the current object is. The main role of the current object is to be first place that names are looked up in. So, if you're looking up x, the top of the subject stack is checked first.

The call stack

The call stack is mostly used for keeping track of your place in code, kinda like Forth's return stack. It is -mostly- not exposed for direct manipulation, but is worth being aware of.

Executing code in StackTalk

A quotation is, roughly, a sequence of names to be looked up, values to be pushed to the data stack, and operations to be performed on those names and values. One thing quotations are -not- is tied to a specific object. An object is a container for a given quotation, not the only scope that quotation will ever execute in.

myStackNameHere is turned into, roughly "look up myStackHere, and 'execute' the value there, if one exists". If the value that is looked up is a quotation or a Zig FFI function, then it is executed in the current state. Otherwise, the value is duplicated from the named stack to the data stack. You can test this with &run

1 &run
1
[ 2 3 + ] &run
5

The one exeception is that &run treats strings on the stack as names to be looked up, and then pushed/executed.

34 >foo foo: &run
34

Defining "functions" in StackTalk

The simplest way to define something like a "function" in StackTalk is to push a quotation onto a stack:

[ 1 + ] >plusOne 2 plusOne
3

StackTalk has a dedicated word for this, fn.

plusOne: [ 1 + ] fn 2 plusOne
3

StackTalk's most common syntax sugar

So, StackTalk has one core syntax sugar that helps make all of this of adjustable readbility. If a quotation is prefixed with a word, the StackTalk compiler re-arranges the output to execute the word after the quotation. So, with that, you can rewrite the example above as:

plusOne: fn[ 1 + ] 2 plusOne
3

Two things should be noted: Because this is implemented in the compiler, neither form has runtime penalties vs the other, and the expectation is that the prefix form will be used in -most- places.

Complex Values, part 2

This finally gives us enough pieces to talk about StackTalk's complex values in more depth. Below is an example of a 2D coordinate:

showPosition: fn[ >y >x "x: " x> + " " + "y: " + y> + println ]
obj[ 10 >x 20 >y ] >position
position .x
position .y showPosition
position .[ x y ] showPosition
position[ x y ] showPosition

With 3 different ways to get data from it. The first is that if a word starts with ., it is interpreted as "move the value at the top of the stack to the top of the me stack, then run the word". So here, .x and .y both do that process.

The second is that when a quotation is prefixed with ., -it- is executed inside the object at the top of the stack, using a very similar process. So, here we see .[ x y ] to get the x and y values out.

Finally, if the value pointed at by the name right before a quotation that has a name prefix is an object, StackTalk will treat that as being the same as if you'd used the .[ ... ] word on the quotation. So here we have position[ x y ] that does that.

Named Stack Operations

In a given object, there are a number of operations that can be performed on the stacks that make it up. The most common is to "run" a given name, as discussed. If you want to force a name to be peeked, you can use . after the name:

[ 1 2 + ] >a 
a println
a. println
3
quote[ 1 2 + ]

If you want to -pop- a value off of a given named stack, you can use > after a word

1 >a a>
1

If you want to check to see if a given named stack is defined for the current object, owns-stack? will tell you

1 >a a: owns-stack? a> a: owns-stack?
true
1
false

If you'd like to update the value on the top of a named stack (or put one there if the stack doesn't currently exist), you can use :name, like so:

1 :a 2 :a a> a: owns-stack?
2
false

Finally, there's let[ ], which allows you to push values to stack, and register those stacks to be popped when the current quotation exits.

[ 1 2 let[ a b ] b a ] &run a: owns-stack? b: owns-stack?
2
1

It should be noted that let does not introduce a new scope, the stacks it pushes to (or introduces) are defined on the current object for the duration of the quotation, and if you execute other quotations, they will be able to see them. There's also fn- that will do the same, but otherwise acts like fn.

Collections

StackTalk has both array and dictionary collections that are available for use.

@[ 1 2 3 ] .[ 1 at ] 
2
@[ 1 2 3 ] .size 
3
@[ 1 2 3 ] do[ 4 push ] .size 
4

Here we see a few operations on an array, as well as a do, which is similar to ., but it returns the value to the top of the data stack after the quotation executes inside of the value. @ is a word that executes a quotation, and the slices all of the newly pushed values from that quotation and gathers them into an array.

&[ a: 5 b: 3 ] .[ b: at ] 
3
&[ a: 5 b: 3 ] .size 
2
&[ a: 5 b: 3 ] do[ 2 c: put ] .size 
4

& is the word for building a dictionary out of the values pushed by a quotation, turning each pair of values into a key/value entry in the dictionary. The one irregularity is that the put word for dictionaries expects the key to be the top of the stack, with the value underneath it, because the key is more often constant and the value is more often a complex expression.

Loops

StackTalk has 3 main looping words. First, we have while:

0 >I while[ I println I: inc I 10 lt? ]
0
1
2
3
4
5
6
7
8
9

while takes a single quotation, and executes it until the value at the top of the stack after executing the quotation is false. inc is another new word here, which takes a string, uses it as a stack name, and tries to add 1 to it.

The next looping word is times:

0 >I 10 times[ I println I: inc ]
0
1
2
3
4
5
6
7
8
9

times is pretty simple, it takes a quotation and a number, and executes the quotation number of times.

Finally, we have the most "generic" looping word: each.

@[ 1 2 3 ] each[ println ]
&[ a: 1 b: 2 ] each[ print " " print println ]
0 9 each[ println ]
1
2
3
b 2
a 1
0
1
2
3
4
5
6
7
8
9

each is able to iterate over arrays, dictionaries, as well as numeric ranges, and its behavior is customizeable. If it can call iter on a given value, it will use value returned by that to drive the logic of how it iterates over the object.

The Lobby: An explicit global object

StackTalk has a designated global scope, in the tradition of SmallTalk and friends, called the Lobby. It's the default object that StackTalk programs start into, though code that is required has a different story (we'll get into that later). If you want a definition to be global to the entire program, the lobby is where to define it:

obj[ 
  Lobby[ 3.14 >PI ]
  PI 2.0 * >TAU
  obj[ PI println TAU println ]
]
3.14
on: obj[ fields: ]
mod: null
Stack does not exist: TAU
error: StackDoesNotExist

This example illustrates another aspect of StackTalk that might be surprising: There is no recursive name lookup. There are only ever three places a name can be found: The current object, the role of the current object (to be explained shortly), and the Lobby.

Another point of note is that if one is making a library (as opposed to a distribution or a StackTalk-driven application), it makes sense to stash most of the contents of the library onto one or more dedicated objects in the Lobby. There are several existing libraries as objects in the default distribution that work this way, including Array, Dictionary, Directory, File and String.

Roles

A role in StackTalk is a bundle of stacks that can be adopted as a lookup fallback for a given object. They are defined via the can word:

greeter: can[ 
  speak: fn[ "Hi, " print name print "!" println ]
]

An object can adopt a role via the is word:

obj[ greeter: is "yumaikas" >name ] >Doortender
Doortender .speak
Hi, yumaikas!

In this respect, roles are sorta like metatables from Lua, or prototypes in JS, except that there aren't any "magic" stack names that can change how name resolution works, just a bunch of stacks that get checked before the lobby. The other difference is that it is both easily possible and expected to -change- the role of an object as it changes over time.

leaver: can[ 
  speak: fn[ "See you later, " print name print "!" println ] 
] 
Doortender[ leaver: is speak ]
See you later, yumaikas!

And if you want to temporarily assume a role, you can use as

Doortender[ greeter: is leaver: as[ speak ] ]
See you later, yumaikas!

There's a few more things that roles can do. The first is that you can use alsoCan to extend an existing role with new definitions:

greeter: alsoCan[ 
  checkIn: fn[ name Building .recordGuest ] 
]

There are also roles that are provided by the standard library, such as array, dictionary, string, boolean, integer and float, which leads into the next topic.

Autoboxing

If you push a value type (aka a string, number or boolean onto the me stack, StackTalk will wrap it in an object, and assign that object the relevant role, in a process called autoboxing. This is mostly there to allow you to do things like "foo" .size, rather than "foo" String .size. While StackTalk has an autoboxed value pushed onto the me stack, the current value is stored in the value stack, and words that manipulate that value, like abs for numbers or skip or take for strings manipulate -that-. If you want to perform a series of mutations in this fashion, the pattern looks something like so:

"foo" do[ 1 skip 1 take ]
"o"

If you do decide to extend the built-in roles like string, I'd recommend making that process opt-in (as opposed to building and extending your own roles).

Format strings

StackTalk has a generic facility for providing format strings. It has a few moving parts. The first, and the way you let StackTalk know you're using one, is to prefix a quotation, or a word/quotation combo with %:

greeter: can[ 
  speak: fn[ %s[ Hi @(name)! ] println ]
]

This is a syntax that the StackTalk compiler takes and recompiles, like so:

[ "Hi " write "name" get "!" write ] %s println

The constant parts of the string are turned into arguments fed to write and the parts of the string prefixed by @ and optionally surrounded by parens (to allow for spaces) are turned into into arguments passed to get. This means that if you define write and get functions in the context that a format string is executed in, you can customize how they are evaluated. %s does this by having write and get append the relevant values to an internal accumulator string. %print and println have write forward to print, and get send the values it picks up to print, allowing you to write out seqeuences of print/println commands without having to do so manually.

Symbol lists

Symbol lists are a handy way to make an array of string constants, supported by the compiler. They are formed by quotations that start with / or prefix words that end with /.

/[ a b c ] each[ dup %println[ @typeof @_ ] ]
string a
string b
string c
aRole: can[ be/[ other roles here ] ]

Predicates

Predicates are how StackTalk manages conditional code execution. They don't have a dedicated syntax, but do have a convention of ending with ?. One thing that predicates -do- do is care about the difference between being prefixed to a quotation or not. To wit:

3 3 eq?
true
3 3 eq?[ foo: ]
"foo"

If a predicate does not have a quotation attached to it, it will leave a boolean on the data stack indicating the condition it is testing. If a predicate -does- have a quoation attached to it, it will execute that quotation if the condition the predicate is testing is true.

The other part of predicates as a system is the word else?. The way that else? works is this: A specific call of a given quotation keeps track of if any predicate with a quotation attached has tested to be true. When else? is run, it only executes if none of the predicates before it tested to true, and then it resets that predicate success state to be unset.

3 2 eq?[ foo: ] else?[ bar: ]
"bar"
3 3 eq?[ foo: ] else?[ bar: ]
"foo"
3 2 eq?[ foo: ] 
3 3 eq?[ bar: ] 
4 3 eq?[ baz: ] 
else?[ quux: ]
"bar"

If you want to make your own predicates, you can use pred, like so:

isThree?: pred[ 3 eq? ]

You can also use pred- if you want to define a predicate that should only exist during the execution of a given quotation.

If you want to write words that change behavior based on if they are being called as a prefix to a quotation, the block? predicate will tell you if the current quotation is being called as a quotation prefix. I don't recommend using this to try to emulate pred, however, unless you want to give yourself a challenge. One example of something that uses it is at on arrays:

/[ a b c d ] .[ 0 at ---: at[ 2 3 ] ]
a
---
c
d
/[ a b c d ] .at[ 0 3 ]
a
d

The last thing that predicates can be used for is exiting the execution of a quotation early. case does this for a supplied quotation:

case[ 
  3 3 eq?[ foo: ]
  2 2 eq?[ bar: ]
  baz:
]
"foo"

breaks does this for the current quotation, which would allow you to rewrite the previous example like so:

breaks
3 3 eq?[ foo: ]
2 2 eq?[ bar: ]
baz:
"foo"

else? will stop the early-exit behavior unless breaks is called again. Similarly to he "any previous predicate succeeded" behavior, the "should exit early" is a bit of state on the current call.

Fried Quotations

Sometimes it is desirable to customize how a quotation behaves based on information that is only known at runtime. StackTalk's tool for this is called Fried Quotations, name borrowed from Factor's notion of the same. That being said, StackTalk's embrace of multiple named stacks means that it does fry specifiers a little differently.

The first thing is that because StackTalk has dedicated support for named stack operations, this extends to fried quotaitons. $name will do the run or peek operation to the value at the top of the name stack, $name. will force the operation to be a peek, and $name> and $name>. will do the same, but when the fried quotation is being built, will pop the value off the named stack. $ will run the value off the top of the data stack. The second thing is that StackTalk uses >[ ] and >word[ ] as the syntax for fried quotations, not '[ ] like Factor does. Some examples:

1 [ 2 ] [ 3 ] >[ $ $ $. ] &run
1
2
quote[ [ 3 ] ]
1 >x [ 2 ] >y [ 3 ] >z >[ $x $y $z. ] &run
1
2
quote[ [ 3 ] ]

Referring to the same named stack more than once won't peek into it

3 >x >[ $x $x ] &run
3
3
3 >x 4 >y >[ $y $x ] &run 
4
3

Trying to fry a quotation in a way that underflows a stack at the time of frying it will cause errors.

2 >x >[ $x> $x ] 
error: StackDoesNotExist

Finally, fried quotations can be passed directly to words, like >fn:

three: 3 >fn[ $ ] three
stilThree: >fn[ $three ]
three: fn[ 4 ]
stillThree
3
3

If you're coming from other languages, fried quotations are closest to closures, with the added wrinkle that they can use up values, akin to linear types

Manipulating the me stack

Even with StackTalk's strict 3-location name lookup, there are still times where you'll want to interact with the object directly below you on the me stack. A few patterns are provided for this.

Prefixing a word with ^ will execute it one level down the me stack from the current object (similar to how prefixing with . will push an object onto the me stack and then execute a word).

2 >X obj[ ^X X: owns-stack? ] #
2 
false

( # is a word that'll drop the top of the data stack )

If you want to run a whole quotation outside of the current object, you can use ^[ ] , like so:

2 >X 3 >Y obj[ ^[ X Y ] X: owns-stack? Y: owns-stack? ]
2 
3
false
false

There is also a version of pred, written as pred^ that will do the same before it executes the relevant fallback. Incidentally, there is also pred- for temporarily defining a predicate for the duration of a quotation call.

Miscellaneous

is-obj adds onto obj to specialize in making Named Objects like Dictionary, StackTalk or TheStack.

Example: is-obj[
  thisIs: >someData
]
Example .someData
"thisIs"

That should be a decent introduction to StackTalk!