Installation

Stack is not currently in the cargo registry, so to install, you must clone the repo and install it manually with cargo.

# Clone the repo
git clone https://github.com/vandesm14/stack

# Move into the directory
cd stack

# After cloning the repo
cargo install --path stack-cli

# Now Stack should be installed
stack --version

Usage

Use stack --help for further documentation.

REPL

You can also use the REPL to run code interactively.

stack repl

Run a file

To run a file, use the run subcommand.

stack run <file>

# or, to watch the file for changes
stack run --watch <file>

Introduction

Stack is a dynamic, stack-based, concatenative programming language.

The goal of Stack is to be an embeddable language similar to that of Lua. Stack inherits many metaprogramming aspects from popular lisps such as Clojure, out-of-the-box. Stack allows the developer to manipulate the code as data at runtime without the need to learn separate syntax for macros. What you see is what you get.

With a native module system, it is possible to extend the runtime with native Rust code, providing higher flexibility for low-level actions.

Getting Started

Head over to the first introductory section on the syntax to get started with Stack.

Syntax

In Stack, there are a few basic types of data, and a few basic ways to structure them. Here are some examples:

;; Integers
1 -1

;; Floats
1.0 -1.0

;; Strings
"Hello, World!" "Hello, \"World!\""

;; Booleans
true false

;; Symbols (aka "Calls")
+ - * / % = < > <= >= != my-symbol what/is_a-symbol?!

;; Lists
'[1 2 3] '[[a pair] [of items]]

;; Lazy
'my-symbol '[] 'fn

;; Functions
'(fn 2 2 +)

;; Records
{key "value" foo "bar"}

;; S-Expressions
(+ 2 2)

;; Comments
;; This is a comment

All whitespace is treated the same, so you have really long one-liners or split each item onto its own line. It's up to you!

Stack

The stack is your workspace. It works as a place to push and pop values, and to call functions. It's the main way to interact with the language.

All expressions are implicitly pushed to the stack.

2 ;; Pushes 2

"hello" ;; Pushes "hello"

'[] ;; Pushes an empty list

Purification

During the phase of introducing a new expression to the program, the engine "purifies" the expression. It will try to evaluate it, such as calling a symbol or evaluating a list.

;; Push 2
2

;; Push 2
2

;; Push + (gets called automatically)
+

;; Returns 4 onto the stack
;; Both 2's and the + are popped (since + was called)
;; [2 2 +] -> [4]

For lists, the items are evaluated in-order, unless the list is lazy.

[2 2 +]

;; Results in `4`
;; [] -> [4]

'[2 2 +]

;; Results in `(2 2 +)`
;; [] -> [(2 2 +)]

Purification only happens once. If a value from a variable is pushed to the stack, it will not be purified. The stack and scope are considered "pure". Only when putting new values into the stack will they be purified.

(Note: Values can only get into the scope from the stack, so no purification step is made when transitioning from stack to scope, only from program/input to stack.)

Laziness

Because the purification step eagerly evaluates symbols and lists, this can be turned off by prefixing ' to the beginning of an expression.

'a

;; Results in the symbol being pushed to the stack, but not called
;; [] -> [a]

As a lazy expression is pushed to the stack, it will be unlazied by one level. So, if you provide two ' in the beginning of an expression, it will be pushed as a lazy expression.

''a

;; Results in the lazy symbol being pushed to the stack and not called
;; [] -> ['a]

Any expression can be made lazy, though it will really only affects symbols, lists, and functions.

2 2 '+

;; Results in both 2's and the + being pushed to the stack, but not called
;; [] -> [2 2 +]

'[2 2 +]

;; Results in the list being pushed to the stack, but not called
;; [] -> [[2 2 +]]

See the docs on lazy lists for more information on the behavior of lazy lists and lists with lazy items.

Calling lists, functions, and symbols will be purified then evaluated. See documentation on the call intrinsic for more information on this behavior.

Variables

Stack includes variables in conjunction with the symbol system. When a symbol is pushed to the stack, the engine will try to call the variable or native function.

Variables are stored in the scope and only exist for the evaluation instance. Native functions such as +, however, don't exist in the scope, but are instead native Rust code that is built with the engine.

It is not possible to redefine native functions, but it is possible to redefine variables.

Defining Variables

Variables are defined using the def operator. The first argument is the name of the variable, and the second is the value.

0 'a def

Notice how a is made lazy with the ' prefix. This prevents a from being called and is placed on the stack as a raw symbol.

'a

;; Results in `a` being pushed to the stack, but not called
;; [] -> [a]

Using Variables

Symbol Calls

You can get the value of a variable by pushing the respective symbol (name of the variable) to the stack.

0 'a def

;; Push the symbol `a` to the stack, which gets called automatically
a

;; Results in `0` being pushed to the stack
;; [] -> [0]

Remember, we don't need to make the second a lazy, because we want it to evaluate to the value.

Updating Variables

Variables can be updated using the set operator. The first argument is the name of the variable, and the second is the new value.

0 'a def

1 'a set

a

;; Results in `1` being pushed to the stack
;; [] -> [1]

If you try to set a variable that doesn't exist, the engine will throw an error.

Lists

Lists are similar to arrays or vectors in other programming languages. A list can contain any number of items of any type.

Defining a List

Lists are defined using the '[] syntax. The items inside of a list are separated by spaces.

'[1 2 3 4 5]

Eager Evaluation

When lists are pushed to the stack, the items inside of the list are evaluated in-order (due to the purification step).

[1 2 3]

;; Results in `1 2 3`
;; [] -> [[1 2 3]] -> [1 2 3]

[2 2 +]

;; Results in `4`
;; [] -> [[2 2 +]] -> [4]

2 'var def

[var]

;; Results in `2`
;; [] -> [[var]] -> [2]

Laziness

Symbols (variables) inside of lazy lists will not be evaluated.

2 'var def

'[var]

;; Results in `[var]`
;; [] -> [[var]]

Instead, to add variables into a list, it will need to be created manually.

2 'var def

var '[] push

;; Results in `[2]`
;; [] -> [[2]]

You can make specific items inside of a list lazy by prefixing ' to the beginning of the item.

[2 2 '+]

;; Results in the items being pushed to the stack, but the `+` will not be not called
;; [] -> [2 2 +]

[2 2 + 5 '*]

;; Results in the items being pushed to the stack, and only the `*` will not be called
;; [] -> [4 5 *]

Calling Lists

Lazy lists can be called which will run each expression in the list in-order (left to right). This allows code to be bundled in a list, and evaluated later.

Calling a list exhibits the same behavior as the purification step.

'[2 2 +] call

;; Results in `4` being pushed to the stack
;; [] -> [2 2 +] -> [4]

If a list contains callable items such as other lists, those will also be run.

'[[2 2 +]] call

;; Results in `4` being pushed to the stack
;; [] -> [2 2 +] -> [4]

To change this behavior, any callable items should be made lazy when adding them to the list to ensure that they won't be called.

'[2 2 +]
;; (2 2 +)
lazy
;; '(2 2 +)
'[] push
;; ('(2 2 +))
call

;; Results in `[2 2 +]` being pushed to the stack
;; [] -> [[2 2 +]] -> [[2 2 +]]

Functions

Though lists are a nice way of bundling code, they aren't perfect. For example, when creating your own named function, you will have to call it manually.

'[1 +] 'add-one def

1 add-one
;; Pushes the list to the stack, but doesn't call it
;; [1] -> [1 [1 +]]

call
;; Calls the list
;; [1 [1 +]] -> [2]

The fn Expression

For this reason, Stack provides parenthetical syntax to create functions. They function similar to lists, except they start with a fn or fn! identifier and use parenthesis instead of square brackets.

'(fn 1 +) 'add-one def

1 add-one
;; Pushes the list to the stack and calls it
;; [1] -> [2]

Notice how we didn't need to call the list manually? That's because the fn expression tells Stack to call the list automatically. This is known as auto-calling.

Note: The evaluation pattern of functions is the same as lists: left to right, evaluating each expression and pushing it to the stack.

Functions are Lists

Though functions are different from lists, you can still use most of the list methods on them (more info here). This means that you can build and modify functions at runtime.

'(fn)
1 push
'+ push
;; [] -> [(fn 1 +)]

'add-one def

1 add-one
;; Pushes the list to the stack and calls it
;; [1] -> [2]

Note: The fn expression needs to be made lazy (with ' -> 'fn) in order for it to not be evaluated. The engine does evaluate the fn expression, but it does nothing on its own (without being wrapped in a list).

Function Calling Behavior

When a function is pulled from scope, it will be auto-called when pushed to the stack. This is the behavior observed above, where add-one called the variable from scope, which Stack evaluated and called the function automatically.

Functions can also be called manually, producing the same behavior that auto-calling does.

For anonymous functions:

'(fn 2 2 +) call

;; Pushes 4 to the stack
;; [] -> [4]

For named functions

'(fn 2 2 +) 'add-two def

'add-two get
;; Pushes the list to the stack
;; [] -> [(fn 2 2 +)]

call
;; Calls the list
;; [(fn 2 2 +)] -> [4]

The get Operator

To get the function itself from the scope, to bypass auto-calling, you can use the get operator.

'(fn a) 'my-fn def

'my-fn get

;; Results in (fn a) being pushed to the stack
;; [] -> [(fn a)]

Scopeless Functions

Normal functions have their own isolated scope, it is not possible to define variables outside of the function's scope. Because of this, Stack includes a "mode" of function called a scopeless function. This is a powerful feature that enables for more dynamic meta-programming.

Scopeless functions don't have their own isolated scope and run in the scope that they are called in. This allows them to define or redefine variables directly in the scope that they were called in.

'(fn! 0 'a def) call

a

;; Pushes 0 to the stack
;; [] -> [0]
;; Create a scopeless function
'(fn! 0 'a def)

;; Create a normal function that calls the scopeless function
'(fn
  call

  ;; `a` is 0
)

;; Call the normal function
call

;; `a` doesn't exist here, since it was part of the previous function's scope

See Scopes for more information on how scoping works and how it relates to normal functions.

Loops

Similar to functional programming languages such as Clojure, Stack utilizes recursion for creating loops. The engine checks if a function pushes the recur symbol to the stack after execution. If recur is detected, the engine will rerun the function, preserving the scope and using tail-call recursion.

;; Define i
0 'i def

;; Function isn't lazy so it runs right away
(fn
  ;; Our if block
  '[
    ;; Push i to the stack
    i

    ;; Add 1 to i
    i 1 + 'i set

    ;; Recur
    recur
  ]

  ;; Check if i is less than 5
  i 5 <

  ;; Run the if
  if
)
;; [0 1 2 3 4]

Note: It is 100% possible to use the stack for incrementing the counter, but for the sake of readability, I used variables for this example.

Scopes

Scopes are one of the most important concepts in Stack as they set Stack apart from traditional concatenative languages. We have put a ton of thought into how they work and how they should behave.

What is a Scope?

Similar to other languages, a scope is a collection of symbol-expression pairs. When a variable is defined, it is added to the current scope. When a variable is called, it is looked up in the current scope (by the purification step).

The Main Scope

The main scope is the top-level of your program. It is the first scope that is created when you start Stack and is the outermost scope.

In the variable section, all examples were in the main scope. Though, the behavior of those examples doesn't change when inside of a scope other than main (such as when within a function).

Creating Scopes

When a function is called, a new scope is created that lives for as long as the function is running. When the function is done, the scope is destroyed.

Scoping Rules

Isolation

Inner scopes are isolated from their outer scope. This means that variables defined in the inner scope are not accessible from the outer scope and will be destroyed when the inner scope is destroyed (unless they are referenced, see closures).

'(fn 0 'a def) call

a
;; Throws an error because `a` is not defined in the main scope

Access and Inheritance

This newly created scope has access to the outer scope. It can read and write to the existing variables in the outer scope. However, if a variable is defined in a inner scope, is will not be accessible from the outer scope.

Therefore, inner scopes have full access to existing variables of the outer scope, but cannot introduce new variables into that (outer) scope.

Getting:

0 'a def

'(fn a) call

;; Pushes 0 to the stack
;; [] -> [0]

Setting:

0 'a def

'(fn 1 'a set) call

a
;; Pushes 1 to the stack
;; [] -> [1]

Shadowing

When a variable is defined in a inner scope with the same name as a variable in the outer scope, the inner scope's variable will "shadow" the outer scope's variable. This means that the inner scope's variable will be used instead of the outer scope's variable. So, the inner scope has access to its own variable while the outer scope's variable is still accessible from within the outer scope.

0 'a def

'(fn 1 'a def a) call
;; Pushes 1 to the stack
;; [] -> [1]

a
;; Pushes 0 to the stack
;; [] -> [0]

Closures

When a variable is referenced in a inner scope, it will be kept alive in the outer scope. This is called a closure and is similar to the behavior in languages such as JavaScript.

;; This function isn't lazy, so it will run automatically
(fn
  0 'a def

  '(fn a)
)
;; (returns the inner function)
;; Pushes `(fn a)` to the stack
;; [] -> [(fn a)]

;; Call the inner function
call
;; Pushes `0` to the stack
;; [(fn a)] -> [0]

As you can see, the inner function still has access to the outer scope's variable a even though the outer function finished executing.

Lispy Syntax

As you have seen, Stack uses postfix notation, meaning the data comes first, then the operations are applied to it. For example:

10 2 -
;; 8

S-Expressions

Stack supports lisp-like (s-expressions) syntax out-of-the-box. For the example above, you could change it to:

(- 10 2)
;; 8

Eager Evaluation

You can also add most operations within the s-expressions, which will be evaluated eagerly:

(- 10 (+ 1 1))
;; 8

(- 10 (fn (def 'a 2) a))
;; 8

Important: Functions and lists need to be lazy when used as the body of an if or let:

if:

;; As the argument: Shouldn't be lazy
(if (fn true) '("hey" print))
;; Prints "hey"

;; As the body: Should be lazy
(if true '(fn "hey" print))
;; Prints "hey"

;; Example with list as the body
(if true '["hey" print])
;; Prints "hey"

;; Example with a lazy-lazy list as the body (returns the list if true)
(if true ''["hey" print])
;; Pushes `["hey" print]` to the stack

;; Example with s-expression as the body
(if true '(print "hey"))

let:

;; BOTH arguments should be lazy (the symbol list and the body)

;; Lists
10 2 (let '[a b] '[a b -])
;; 8

;; Functions
10 2 (let '[a b] '(fn a b -))
;; 8

;; S-Expressions
10 2 (let '[a b] '(- a b))
;; 8

This is due to the eager evaluation, where both functions, lists, and s-expressions, if unlazied, will be called during eager evaluation. This can be useful for lambdas within the arguments of an s-expression

The Underscore

To include items from the stack within an s-expression, you can use _. The underscore will pop the last item from the stack and use it as the argument in its place.

10 2
;; b a
(- _ _)
;; (- a b) -> (- 2 10) -> -8

In this example, you can see that the underscores don't order the arguments as they are visually. Instead, as the evaluator goes from left to right, they pop an item from the stack. The first underscore will pop the last item from the stack and the second underscore will pop the second-to-last item from the stack.

Parity with Stack-based syntax

Stack's lispy syntax is one-to-one with the visual ordering of all intrinsics, except for a select few, which we will talk about in a bit.

For example, these expressions are all equivalent:

10 2 -
;; 8

(- 10 2)
;; 8

10 (- _ 2)
;; 8

2 (- 10 _)
;; 8

Exceptions

For ergonomics, Stack modifies the syntax for list and record intrinsics.

List of Exceptions:

  • push
  • insert
  • let
  • def
  • set

In this case, these push operations are equal:

0 '[] push
;; [0]

(push '[] 0)
;; [0]
"value" "key" {} insert
;; {key: "value"}

(insert {} "key" "value")
;; {key: "value"}

In these cases, the arguments for the s-expressions are reversed in comparison with the stack-based syntax. This is to aid in readability and intuition. The non-lisp syntax doesn't do this for aid in more efficient insertions or pushes:

3 2 1 '[] push push push
;; [1 2 3]

"bar" "foo" "value" "key" {} insert insert
;; {key: "value", foo: "bar"}

That said, pulling items from the stack works the same as with the other intrinsics:

{}
(insert _ "key" "value")
;; {key: "value"}

"key" {}
(insert _ _ "value")
;; {key: "value"}

"value" {}
(insert _ "key" _)
;; {key: "value"}

"value"
(insert {} "key" _)
;; {key: "value"}

Here are examples for the last few exceptions:

Let:

10 2 '[(- a b)] '[a b] let
;; 8

10 2 (let '[a b] '(- a b))
;; 8

Def:

0 'a def

(def 'a 0)

0 'a (def _ _)

Set:

;; define so we can set
0 'a def
;; end of boilerplate

1 'a set

(set 'a 1)

1 'a (set _ _)

Built-Ins

Stack comes with quite a few built-in functions (called intrinsics internally). They provide baseline functinoality for Stack.

Lists vs Functions

Unless explicitly stated in the description of a function, for all mentions of the type list, functions are also supported (e.g.: (fn 2 2 +) len).

Arithmetic

Note: Stack uses wrapping arithmetic, unlike Rust, which uses bounds checking in safe modes.

Add (+)

Signature: ([a: int] [b: int] -- int)

Equivalent Rust: a + b

Examples:

1 2 +
;; 3

Subtract (-)

Signature: ([a: int] [b: int] -- int)

Equivalent Rust: a - b

Examples:

2 1 -
;; 1

1 2 -
;; -1

Multiply (*)

Signature: ([a: int] [b: int] -- int)

Equivalent Rust: a * b

Examples:

2 3 *
;; 6

Divide (/)

Signature: ([a: int] [b: int] -- int)

Equivalent Rust: a / b

Examples:

6 3 /
;; 2

Remainder (%)

Signature: ([a: int] [b: int] -- int)

Equivalent Rust: a % b

Examples:

10 5 %
;; 0

11 5 %
;; 1

Comparison

Equal (=)

Signature: ([a] [b] -- bool)

Equivalent Rust: a == b

Examples:

2 2 =
;; true

"hello" "world" =
;; false

'(1 2) '(1 2) =
;; true

Not Equal (!=)

Signature: ([a] [b] -- bool)

Equivalent Rust: a != b

Examples:

2 2 !=
;; false

"hello" "world" !=
;; true

'(1 2) '(1 2) !=
;; false

Less Than (<)

Signature: ([a] [b] -- bool)

Equivalent Rust: a < b

Examples:

1 2 <
;; true

2 1 <
;; false

Less Than or Equal To (<=)

Signature: ([a] [b] -- bool)

Equivalent Rust: a <= b

Examples:

1 2 <=
;; true

2 2 <=
;; true

2 1 <=
;; false

Greater Than (>)

Signature: ([a] [b] -- bool)

Equivalent Rust: a > b

Examples:

1 2 >
;; false

2 1 >
;; true

Greater Than or Equal To (>=)

Signature: ([a] [b] -- bool)

Equivalent Rust: a >= b

Examples:

1 2 >=
;; false

2 2 >=
;; true

2 1 >=
;; true

Boolean

Or (or)

Signature: ([a: bool] [b: bool] -- bool)

Equivalent Rust: a || b

Examples:

false false or
;; false

false true or
;; true

true false or
;; true

true true or
;; true

And (and)

Signature: ([a: bool] [b: bool] -- bool)

Equivalent Rust: a && b

Examples:

false false and
;; false

false true and
;; false

true false and
;; false

true true and
;; true

Not (not)

Signature: ([a: bool] -- bool)

Equivalent Rust: !a

Examples:

false not
;; true

true not
;; false

Stack Ops

Drop (drop)

Signature: ([a] --)

Drops a from the stack

Examples:

"hey"
;; ["hey"]

drop
;; []

Duplicate (dupe)

Signature: ([a] -- a a)

Duplicates a on the stack

Examples:

"hey"
;; ["hey"]

dupe
;; ["hey" "hey"]

Swap (swap)

Signature: ([a] [b] -- b a)

Swaps a and b on the stack

Examples:

"hello" "world"
;; ["hello" "world"]

swap
;; ["world" "hello"]

Rotate (rot)

Signature: ([a] [b] [c] -- b c a)

Rotates a, b, and c on the stack

Examples:

"a" "b" "c"
;; ["a" "b" "c"]

rot
;; ["b" "c" "a"]

Lists

Length (len)

Signature: ([a: list|string|function] -- int)

Equivalent Rust: a.len()

Examples:

'[1 2 3] len
;; 3

"123" len
;; 3

Get at Index (nth)

Signature: ([a: list|function] [b: int] -- a any) or ([a: string] [b: int] -- a string)

Equivalent Rust: a[b] or a.get(b)

Examples:

'[1 2 3] 0 nth
;; [[1 2 3] 1]

'[1 2 3] 2 nth
;; [[1 2 3] 3]

"123" 0 nth
;; ["123" "1"]

"123" 2 nth
;; ["123" "3"]

Split (split)

Signature: ([a: list] [b: int] -- list list) or ([a: string] [b: int] -- string string)

Splits a at the separator b and returns both chunks.

Examples:

'[1 2 3] 1 split
;; [1] [2 3]

"123" 1 split
;; "1" "23"

Concat (concat)

Signatures:

  • ([a: list] [b: list] -- list)
  • ([a: string] [b: string] -- string)
  • ([a: function] [b: function] -- function)
  • ([a: function] [b: list] -- function)
  • ([a: list] [b: function] -- list)

Concats a and b together (concats the two lists or two strings)

Examples:

'[1] '[2 3] concat
;; [1 2 3]

"1" "23" concat
;; "123"

Push (push)

Signature: ([a] [b: list|function] -- b) or ([a: string] [b: string] -- string)

Equivalent Rust: b.push(a)

Examples:

3 '[1 2] push
;; [1 2 3]

"3" "12" len
;; "123"

Pop (pop)

Signature: ([a: list|function] -- a any) or ([a: string] -- string)

Equivalent Rust: a.pop()

Examples:

'[1 2 3] pop
;; 3

"123" len
;; "3"

Records

Insert (insert)

Signature: ([value] [key] [c: record] -- record)

Equivalent Rust: c.insert(key, value)

Examples:

"value" "key" {} insert
;; {key: "value"}

true 'key {} insert
;; {key: true}

2 1 {} insert
;; {1: 2}

Property (prop)

Signature: ([a: record] [b] -- a any)

Equivalent Rust: a.get(b)

Examples:

{key "value"} "key" prop
;; [{key "value"} "value"]

{key "value"} "foo" prop
;; [{key "value"} nil]

{key "value"} 'key prop
;; [{key "value"} "value"]

{key "value"} 'foo prop
;; [{key "value"} nil]

{1 2} 1 prop
;; [{1 2} 2]

{1 2} 2 prop
;; [{1 2} nil]

Has (has)

Signature: ([a: record] [b] -- a bool)

Equivalent Rust: a.has(b)

Examples:

{key "value"} "key" has
;; [{key "value"} true]

{key "value"} "foo" has
;; [{key "value"} false]

{key "value"} 'key has
;; [{key "value"} true]

{key "value"} 'foo has
;; [{key "value"} false]

{1 2} 1 has
;; [{1 2} true]

{1 2} 2 has
;; [{1 2} false]

Remove (remove)

Signature: ([a: record] [b: string] -- record)

Equivalent Rust: a.remove(b)

Examples:

{key "value" foo "bar"} "foo" remove
;; [{key "value"}]

{key "value" foo "bar"} "bar" remove
;; [{key "value" foo "bar"}]

Keys (keys)

Signature: ([a: record] -- a list(symbol))

Equivalent Rust: a.keys()

Examples:

{key "value" foo "bar"} keys
;; [{key "value" foo "bar"} (key foo)]

{"key" "value" "foo" "bar"} keys
;; [{key "value" foo "bar"} (key foo)]

Values (values)

Signature: ([a: record] -- a list)

Equivalent Rust: a.values()

Examples:

{key "value" foo "bar"} values
;; [{key "value" foo "bar"} ["value" "bar"]]

{f (fn 2 2 +)} values
;; [{key (fn 2 2 +)} ((fn 2 2 +))]

{f '(fn 2 2 +)} values
;; [{key '(fn 2 2 +)} ('(fn 2 2 +))]

Types

Cast (cast)

Signature: ([a] [b: string] -- any)

Converts a to the type: b and returns the new type

Type of (typeof)

Signature: ([a] -- string)

Gets the type of a and pushes it as a string to the stack

Lazy (lazy)

Signature: ([a] -- lazy(a))

Wraps a with a lazy expression, making it lazy.

Examples:

1 lazy
;; '1

'[]
;; []
lazy
;; '[]

Control Flow

If (if)

Signature: ([a: bool] [b: list] --)

Equivalent Rust: if a { b }

Examples:

'["true"]
true
if
;; "true"
[4 4 =]
;; [true]
'["true"]
;; [true ["true"]]
if
;; ["true"]

Halt (halt)

Signature: (--)

Equivalent Rust: Halts execution.

Examples:

2 2 halt +
;; halts before the "+"

Recur (recur)

Signature: (-- symbol)

A QoL helper intrinsic that pushes the symbol: recur to the stack. Used to allow recur to be called without escaping with a lazy (such as 'recur).

Examples:

;; Define i
0 'i def

;; Function isn't lazy so it runs right away
(fn
  ;; Check if i is less than 5 (condition)
  i 5 <

  ;; Our if block
  '[
    ;; Push i to the stack
    i

    ;; Add 1 to i
    i 1 + 'i set

    ;; Recur
    recur
  ]

  ;; Run the if
  if
)
;; [0 1 2 3 4]

OrElse ('orelse')

Signature: ([a] [b] -- a|b)

Equivalent Rust: a.or(b)

If a is nil, returns b. Else, returns a.

Examples:

nil 2 orelse
;; 2

1 2 orelse
;; 1

Scopes and Variables

Define (def)

Signature: ([a] [b: symbol] --)

Equivalent Rust: let b = a

Examples:

0 'a def
a
;; 0

'(fn +) 'add def
2 2 add
;; 4

Set (set)

Signature: ([a] [b: symbol] --)

Equivalent Rust: b = a

Examples:

0 'a def
1 'a set
a
;; 1

1 'a set
;; throws since `a` is not defined

Call (call)

Signature: ([a] --)

Calls a and:

  • If a is a function: Runs the function
  • If a is a list: Runs each item in the list
  • If a is a symbol: Calls the symbol from the scope
  • If a is anything else: Pushes it back onto the stack

Examples:

2 2 
'(fn +) call
;; 4

'[2 2 +] call
;; 4

'[2 2 +] 'add def
add
;; [[2 2 +]]
call
;; [4]

'(fn +) 'add def
2 2 'add call
;; 4

0 'a def
'a call
;; 0

"foo" 'a def
'a call
;; "foo"

Let (let)

Signature: ([a: list] [b: list(symbol)] --)

Pops b.len() items off of the stack, assigning each item the corresponding symbol in b. Then, runs the code block a, injecting the symbols into the scope.

If list b was (first second), then they would be popped from the stack in order, following this signature: ([first] [second] --).

Important Note: Functions cannot be used as the block of a let (a). To use functions within lets, wrap them within the let block: 0 '((fn a)) '(a) let. Lets create create their own scopes, so any def will be isolated to that let.

Examples:

10 2 '[a b -] '[a b] let
;; 8

10 2
'[
  (fn a b -)
] '[a b] let
;; 8

10 2
(fn
  '[a b -]
  '[a b]
  let
) call
;; 8

Get (get)

Signature: ([a: symbol] -- any)

Equivalent Rust: a

Examples:

0 'a def
'a get
;; 0

'(fn +) 'add def
2 2 add
;; 4

'(fn +) 'add def
'add get
;; (fn +)

'(fn +) 'add def
2 2 
'add get call
;; 4

Debugging and I/O

Debug (debug)

Signature: ([a] -- a)

Equivalent Rust: dbg!(format!("{}", a))

Examples:

0 debug
;; prints 0

"hey" debug
;; prints "hey"

Assert (assert)

Signature: ([a] [b: bool] -- a)

Equivalent Rust: assert!(b, format!("{}", a))

Examples:

"my test" 2 2 = assert
;; nothing (it passes)

"my test" 1 2 = assert
;; error: assertion failed caused by my test

Import (import)

Signature: ([a: string] --)

Runs the file from path a in the current environment. Variables and stack changes will persist from file a.

Examples:

;; lib.stack
'(fn +) 'add def

;; main.stack
"lib.stack" import
2 2 add
;; 4

Installation

Stack comes with a debugger that provides a GUI to aid in debugging and inspecting the behavior of Stack.

# Clone the repo
git clone https://github.com/vandesm14/stack

# Move into the directory
cd stack

# After cloning the repo
cargo install --path stack-debugger

# Now the debugger should be installed
stack-debugger --version

Usage

Use stack-debugger --help for further documentation.

Run a file

To debug a file, provide it with a path.

stack-debugger <file>

The debugger automatically watches the file for changes and reruns the code.

Usage

TODO: add usage docs (with screenshots)