A nice looking functional, concatenative programming language for minimal systems.
By Kroc Camen.
- Simple left-to-right push-down parser. No look-ahead!
Intended for assembling & execution on very constrained systems - Easy to tokenise: anything separated by spaces is a "function"
# a comment begins with a hash mark and a space
# -- the space is required because the hash mark is a
# function that reads the rest of the line and discards it
A constant is a function that always returns the same value.
let true 1
let false 0
A variable is a function that returns its current value.
It must be defined ahead of time and must have a default value.
var is_the_queen_dead false
A variable's value is changed with the set
function.
set is_the_queen_dead true
let DECIMAL 0
let HEXADECIMAL $01
let BINARY %00000001
let FLOAT 1.0
Arithmetic is done purely left-to right, there is no operator precedence. The result of an infix calculation (e.g. 4 + 5) is totalled before proceeding to the next operator (e.g. * 3). This behaviour is intentional for simplicity of parsing, particularly by 8-bit CPUs and almost entirely does away with the need to nest parentheses.
Since there is no look-ahead, the brackets are required to indicate an expression that must be evaluated to produce the result -- an expression can be thought of a small inline function that produces a value.
set number ( 4 + 5 * 3 )
set number ( number + 1 )
An expression is the only place operators may be used.
let ADD ( 1 + 1 )
let SUBTRACT ( 1 - 1 )
let MULTIPLY ( 1 * 1 )
let DIVIDE ( 1 / 1 )
let EXPONENT ( 2 ^ 1 )
let MODULO ( 10 % 2 )
let LOGICAL_OR ( 0 or 1 )
let LOGICAL_AND ( 1 and 1 )
let BINARY_OR ( 0 | 0 )
let BINARY_AND ( 0 & 0 )
let BINARY_XOR ( 0 ~ 0 )
let EQUAL ( 0 = 0 )
let NOT_EQUAL ( 0 != 0 )
let LESS ( 0 < 0 )
let GREATER ( 0 > 0 )
let LESS_EQUAL ( 0 <= 0 )
let GREATER_EQUAL ( 0 >= 0 )
let greeting "Hello, World!"
A lambda is a fixed, immutable list of values. A "value" is a number, a string, an expression, a function, other lambdas, and any other types.
Lambdas begin with :
and end with ;
.
: cat sit mat … ;
Any functions calls or expressions in the lambda are not evaluated until execution; the expressions are stored in the lambda in a frozen, uncalculated state.
A function is a lambda with a name.
A function is defined by a name and a lambda of instructions:
fn three :
( 1 + 2 )
;
Functions do not define a parameter list up-front, instead they take their parameters from the instruction stream when desired using the get
function. Therefore a function could read a different number of parameters depending on what's read!
fn add :
get first
get second
( first + second )
;
echo add 1 2 # prints "3"
Note how functions return values by evaluation. So long as a value is not being used as a parameter to a function call, it is returned from the function.
Local variables (and constants) can be defined within functions and exist only within the function scope. The get
function acts the same as var
, defining the variable, but also retrieving the parameter at the same time.
fn add :
get first
get second
var third ( first + second )
third
;
echo add 1 2 # prints "3"
An if
block takes any value, including an expression, and a lambda to execute if the value resolves to true.
fn max :
get first
get second
if ( first > second ) :
first
exit # exits a function early
;
second
;
For if-then-else constructs, the function if-else
takes a value and two lambdas, the first is executed if the value resolves to true and the second is executed otherwise.
fn max :
get first
get second
if-else ( first > second ) :
first
; :
second
;
;
The true & false parameters do not need to be lambdas, they can be function calls or even values to return:
fn min :
get first
get second
if-else ( first > second ) first second
;
TODO: switch / match
while ( expression ) :
⋮
;
do :
⋮
exit
;
TODO: for loops
Lists are dynamically generated and managed lists of values.
If lambdas are functions as constants then lists are functions as variables.
A list is defined by square brackets, either closed for an empty list, or containing a number of default values.
var empty_list []
var three_list [ 1 2 3 ]
Unlike lambdas, expressions and function calls will be evaluated when defining the list. A list can be thought of as a function that allocates memory for a list and then begins populating the list with each value it comes across.
Functions for manipulating lists exist, but these are library functions rather than intrinsic syntax so I won't got into detail here.
count list # return number of values in list
first list # return first value in list
last list # return last value in list
push list value # add value to end of list
pop list # remove (+return) last value in list
prepend list value # add value to start of list
insert list index value # insert value at index
replace list index value # replace value at index
remove list index # remove value at index
join list list # join two lists together as one
slice list index length # slice list starting at index
Accessing an index of a list is done with the @
operator.
var numbers [ 1 2 3 ];
echo ( numbers @ 2 ) # prints "2"
Up to this point we've been avoiding an important implementation detail that makes Pling! different; it has an implicit data stack.
This means that, as well as parameters, functions can work on data that is pushed to and popped from a data stack. Unlike parameters, this data persists as we move across functions. This allows Pling! to work with both static and dynamic data types.
The data stack is always separate from the function return stack and any other implementation-specific stacks.
Values returned by functions are being pushed on to the data stack, ergo a function can return more than one value:
fn potatoes :
1
2
;
When we call a function with a parameter, such as echo
, what we are really saying is that echo
will print the result on top of the stack of what the following value / function evaluates to.
echo sir_lancelots_favourite_colour
Pling! is so named because an exclamation mark (also known as a "bang" or "pling") is a function that pops the top item off the stack instead of pushing something new on. It can be used as a replacement for parameters!
1 # push the value "1" on to the data stack
echo ! # pop a value off the data stack and print it
Each value on the stack is opaque. It's important to understand that if you push a list on to the data stack, you will pop the entire list, not each item one-by-one:
[ 1 2 3 ] # push a list on to the stack
echo ! # prints "[ 1 2 3 ]"!
You can temporarily move the data pointer into the list using a with
block:
4 # note how the stack is first-in, last-out
[ 1 2 3 ] # this will be on top of the stack
with ! :
echo ! # prints "1"
echo ! # prints "2"
;
echo ! # prints "4"
You can also take a list and iterate over it. The each
function takes a list as a parameter (or, with !
, the stack) and calls a function / lambda for each value in the list, automatically pointing the data parameter at the popped value.
[ 1 2 3 ]
each ! : echo ! ; # prints "1", "2", "3"
If a list is nested however, we don't automatically get recursion:
[ 1 2 [ 3 4 ]]
each ! : echo ! ; # prints "1", "2", "[ 3 4 ]"
The map
function calls a function for each value in a list and will handle the recursion for us. Note how we can also do away with the lambda since the map
function takes a function name as a 2nd parameter.
[ 1 2 [ 3 4 ]]
map ! echo # prints "1", "2", "3", "4"
The ?
function 'peeks' the stack value, but does not pop it. You can use this when you want to get the value atop the stack, but don't want to remove it.
[ 1 2 3 4 ]
map ? echo # prints "1", "2", "3", "4"
echo count ? # prints 4
The .
function throws away (or "drops") the value atop the stack.
Use this when you need to level the stack for parameters you don't use.
1 2 3 4 # 4 items on stack, not a list
. . . # drop three items
echo ! # prints "1"
In Forth it's easy to make mistakes where you put one value on the stack but you accidentally read it back and treat it as something it's not. Forth's lack of a type system exposes its unforgiving nature for beginners, or just feeling your way through a problem. Most modern Forth-like languages therefore include a type system.
Everything in Pling! is a list of values.
Values can be of any type:
- A number
- A string
- An expression, a kind of list specific to operators
- A lambda -- a statically assembled list
- A list -- a dynamically allocated list
- A function name
- A structure
We've covered all but the last type in some way or another thus far.
A data type can be thought of as a class in other programming languages. Each data type has to have methods for printing and for pushing and popping from the stack.
In Pling!, data types are the lowest-level primitives that are typically implemented in machine code. How the data is stored and retrieved is highly machine-specific, however Pling! programs don't ever deal with the implementation details directly.
The type of a value is bound to it. If you push a number to the data-stack you can not read it back as a function name. Whatever is pushed will always pop as the same type as it was before.
(in progress...)