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)