Experimental stack based programming language
- Install Rust using https://rustup.rs/
- Clone this repo
cd /path/to/scatter-langcargo install --path .
Once installed, Scatter programs can be made into executables by adding #!/usr/bin/env scatter to the start of the file and running chmod +x ./program.sl. This will allow you to run ./program.sl directly.
Execute a Scatter file (.sl extension):
scatter program.slRun multiple files in sequence:
scatter file1.sl file2.sl file3.slLaunch the interactive REPL by running without arguments:
scatter-a, --analyze- Analyze code and perform type checking instead of executing-g, --generate <language>- Generate code, see Code Generation below.
Examples:
# Type check a program
scatter -a program.sl
# Generate JavaScript code for the given program
scatter -g js program.sl// Literals push values onto the stack
42 true "hi" // [42, true, "hi"]
// Operations work in reverse polish notation - taking values from the stack and placing the results back on
3 // [3]
4 // [3, 4]
5 // [3, 4, 5]
+ // [3, 9]
+ // [12]
// Functions are defined with `name: {body}` and called simply by referencing their name
square: {dup *}
5 square // [25]
// Branches use conditions to choose which block to execute
sign: {
{
(dup 0 >) "positive"
(dup 0 <) "negative"
(dup 0 ==) "zero"
(1) "NaN"
}
swap drop // Drop the passed in value
}
4 sign // ["positive"]
-1 sign // ["negative"]
-1 0.5 ** sign // ["NaN"]
// Loops re-run a block until an exit condition is reached
countdown: {
[
(dup)
dup
--
]
}
5 countdown // [5, 4, 3, 2, 1]
The examples/ directory contains various Scatter programs demonstrating different features:
Run any example with:
scatter ./examples/fibonacci.slThe stack is the core data structure in Scatter. All values exist on the stack, and logic manipulates the stack to transform data. There are no variables, all state is on the stack.
The stack grows as items are pushed - newer items are pushed to the end:
3 // [3]
4 // [3, 4]
5 // [3, 4, 5]
Stack operations consume their operands:
5 3 // [5, 3]
- // [2] - removes 5 and 3 from stack, leaves 2
Functions operate on the stack, taking arguments from the stack as needed:
double: {
2 *
}
4 double // [8]
Literals push values directly onto the stack:
// Numbers (doubles)
42
3.14
-17
// Booleans (true/false)
true
false
// Strings
"hello"
"world!"
""
"\"" // Escaped quote character
Each value on the stack is either a number, string, or boolean. Some intrinsics accept any type, but arithmetic operations will exit with an error if they are not used on numbers.
Values can be converted to booleans either by conditions or by boolean operators, in which case false, 0, NaN, and "" are considered falsy (equivalent to false), and all other values are considered truthy (equivalent to true).
Intrinsics are built-in operations predefined in the language.
+ // Addition: a b -> (a + b)
- // Subtraction: a b -> (a - b)
* // Multiplication: a b -> (a * b)
/ // Division: a b -> (a / b)
% // Modulo: a b -> (a % b)
** // Exponentiation: a b -> (a ** b)
++ // Increment: a -> (a + 1)
-- // Decrement: a -> (a - 1)
== // Equal: a b -> (a == b)
< // Less than: a b -> (a < b)
> // Greater than: a b -> (a > b)
&& // Logical AND: a b -> (a && b)
|| // Logical OR: a b -> (a || b)
! // Logical NOT: a -> (!a)
Note that in the case of || and &&, types are preserved:
3 4 || // [3]
0 1 || // [1]
"hi" "" || // ["hi"]
"" 0 || // [0]
3 4 && // [4]
0 1 && // [0]
"hi" "" && // [""]
"" 0 && // [""]
substring // Extract substring: string start end -> substring
join // Concatenate values: a b -> "ab"
length // Get string length: a -> length(a)
to_char // Convert character to ASCII: "c" -> 99
from_char // Convert ASCII to character: 99 -> "c"
index // Find substring position: haystack needle -> position (or -1)
dup // Duplicate top value: a -> a a
swap // Swap top two values: a b -> b a
over // Copy second value to top: a b -> a b a
rot // Rotate top three values: a b c -> b c a
drop // Remove top value: a b -> a
readline // Read a line of input, returns [string, boolean]. The boolean is true if the input stream is still open.
print // Print the top value to the screen
assert // Assert condition is truthy: condition message -> (fails if condition is falsy)
Functions are named blocks of code that operate on the stack. They have no explicit parameters - they work with whatever is on the stack when called. To return a value, functions place the result on the stack for the caller to use.
name: {
...
}
// Single line functions are also supported
print_top: dup print
square: dup *
5 square // [25]
add_ten: 10 +
7 add_ten // [17]
greet: {
"Hello" print
}
greet // [], "Hello" printed
Functions can take any number of arguments, based on what is already in the stack. If there are insufficient values on the stack, an error will be thrown.
square: dup *
sqrt: 0.5 **
// x1 y1 x2 y2 -> distance
distance: {
rot - square // evaluate (y2 - y1) ^ 2
rot rot // bring x1 and x2 to the top of the stack
- square // evaluate (x2 - x1) ^ 2
+ // add the previous results
sqrt
}
3 0 // [3, 0], define first point
0 4 // [3, 0, 0, 4], define second point
distance // [5]
Branching provides conditional execution. Branches evaluate conditions top-to-bottom and execute the first matching case.
{
(condition1) action1
(condition2) action2
}
Conditions contain an expression that is evaluated to make a control flow decision. If the contained expression evaluates to a truthy value, the associated action executes. The empty condition () evaluates what is currently on the top of stack and consumes it. (1) can be used as a "else" case which always evaluates to true.
check_even: {
{
(2 %) "Odd"
(1) "Even"
}
}
3 check_even // ["Odd"]
4 check_even // ["Even"]
grade: {
{
(dup 60 <) "F"
(dup 70 <) "D"
(dup 80 <) "C"
(dup 90 <) "B"
(dup 100 <) "A"
(1) "A+"
}
swap drop // remove the score, leaving just the grade
}
83 grade // ["B"]
Loops provide the ability to run code repeatedly. It consists of an optional pre-condition, a body, and an optional post-condition.
The pre-condition is checked at the start of each iteration, and the post-condition is checked after each iteration. If either condition returns a falsy value, the loop exits. A loop with no conditions will repeat indefinitely.
[(pre_condition) body (post_condition)]
- Check pre-condition if it exists - if false, exit
- Execute body
- Check post-condition if it exists - if false, exit
- Go back to step 1
countdown: {
[
(dup) // Check if the current value is positive, exit otherwise
dup print // Print the current value
-- // Decrement the value
]
drop // Consume the value
}
10 countdown // Prints numbers 10 down to 1
fibonacci: {
0 1
[
(rot dup) // Check if the counter is down to 0
1 - // Subtract one from the counter
rot rot // Put the counter back at the bottom of the stack
dup rot + // Get the next number in the sequence
]
drop drop // Leave the stack with just the result
}
20 fibonacci // [6765]
factorial: {
1 swap // Put 1 on stack as accumulator, counter on top
[
(dup) // Exit if counter is down to 0
dup rot * // Multiply accumulator by current counter
swap 1 - // Decrement counter
]
drop // Remove the counter, leave result
}
5 factorial // [120]
When // occurs outside a string, the remainder of the line is ignored.
1 // 2 +
The 2 + is in the comment so it is ignored.
Files can be imported using the # symbol. There are 3 types of imports: wildcard, named, and scoped.
To pull in all functions, use wildcard. To pull in a subset of the functions in a file, use named. To access the functions within a prefix, use scoped.
// Wildcard
# * "./path"
// Named
# {name1 name2} "./path"
// Scoped
# scope "./path"
// math.sl
square: dup *
// Using wildcard
# * "./math.sl"
5 square // [5]
// Using named
# {square} "./math.sl"
15 square // [225]
// Using scoped
# math "./math.sl"
10 math.square // [10]
Scatter will exit with an error message on:
- Stack underflow (trying to pop from an empty stack)
- Type mismatches (e.g., using arithmetic operations on strings)
- Failed assertions with
assert - Invalid operations (e.g.,
to_charon multi-character strings)
Errors display a descriptive message and cause the program to terminate with a non-zero exit code.
In addition to the interpreted mode, Scatter source code can be converted to source code in other languages. Javascript and C are currently supported. The code will be outputted to stdout so the code can either be directed to a file, or piped directly into node for example.
scatter --generate js examples/fizzbuzz.sl | nodescatter --generate c examples/fizzbuzz.sl > /tmp/fizzbuzz.c
gcc -o /tmp/fizzbuzz /tmp/fizzbuzz.c
/tmp/fizzbuzz