Crates.io | spl |
lib.rs | spl |
version | 0.4.1 |
source | src |
created_at | 2023-02-20 12:32:40.164801 |
updated_at | 2024-11-22 20:38:01.183399 |
description | Stack Pogramming Language: A simple, concise scripting language. |
homepage | |
repository | https://github.com/tudbut/spl |
max_upload_size | |
id | 789788 |
size | 247,616 |
SPL is a simple, concise, concatenative scripting language.
Example:
func main { mega | with args ;
"Running with args: " print
args:iter
{ str | " " concat } swap:map
&print swap:foreach
"" println
println<"and with that, we're done">
[ ^hello ^pattern-matching ] => [ ^hello &=args ] if {
args println "prints pattern-matching";
}
0
}
def
introduces a variable.
def a
Writing a constant pushes it to the stack. This works with strings and numbers.
"Hello, World!"
Use =<name>
to assign the topmost value to a variable. In this case, that is
"Hello, World!"
=a
This can be written as a single line - line breaks are always optional, and equal to a space.
def a "Hello, World!" =a
Variables consist of two functions: <name>
and =<name>
. Use <name>
to
obtain the value again.
a
The print
function is used to print a value. It takes one value from the stack
and prints it without a newline. To print with a newline, use println
. The
semicolon at the end means 'if this function returns anything, throw it away'.
This can be used on strings to make them comments, but is not available for
numeric constants.
println;
Hello, World!
The func
keyword introduces a function. The { mega |
is the return type
declaration, which in SPL is done within the block. In this case, our function
returns one of the mega
type, which is a 128-bit integer.
func main { mega |
The with
declaration will be explained below. It defines the args
argument.
with args ;
Now, we can write code like before:
def list
SPL has a varying-length array type, the list. To create any construct (object),
we use :new
.
List:new =list
To add to the end of a list, we push
to it. All construct methods are
written with a colon, like before in the new
example.
"Hello," list:push
Note the lowercase list
, because we are pushing to the construct in the
variable.
Now, let's also push "World!".
"World" list:push
Beautiful. I'd like to print it now, but how?
We can't print a list directly (with what we know so far), but we can iterate through it!
{ | with item ;
item print;
" " print;
} list:foreach;
"" println;
There is a lot to unpack here!
{ |
creates a closure with no return type (in C-style languages, that'd be
a void function).with item ;
declares arguments. This is optional, and not needed if the
function does not take arguments. Running "a" "b" "c"
and calling
something with a b c ; will leave each letter in the corresponding variable.}
ends the closure, and puts it on the top of our stack.list:foreach
calls the foreach
method on our list
, which is declared
with callable this ; - that means we need to provide one argument along with
the implied this
argument (it can have any name - the interpreter does not
care about names in any way - this
is just convention). The callable
here is not a type!foreach
also does not return anything, but I added the semicolon for
clarity.Hello, World!
SPL has Ranges, constructed using <lower> <upper> Range:new
. You can iterate
over them.
0 5 Range:new:iter
Now, let's multiply all of these values by 5.
{ mega | 5 * } swap:map
Wait, what? Why is there suddenly an inconsistency in method calls, the iterator isn't being called, it's something else now!
It sure does look like it, doesn't it? swap
swaps the topmost two values on
the stack. a b -> b a
. That means we are actually calling to our iterator.
The closure and the iterator are swapped before the call is made. swap:map
is a more concise way of writing swap :map
.
The map function on the iterator (which is available through :iter
on most
collection constructs) is used to apply a function to all items in the
iterator. The closure here actually takes an argument, but the with
declaration is omitted. The longer version would be:
{ mega | with item ;
item 5 *
}
But this is quite clunky, so when arguments are directly passed on to the next
function, they are often simply kept on the stack. The *
is simply a
function taking two numbers and multilying them. The same goes for +
, -
,
%
, and /
. a b -
is equivalent to a - b
in other languages. lt
,
gt
, and eq
are used to compare values.
Returning is simply done by leaving something on the stack when the function
exits, and the return declaration can technically be left off, but the
semicolon won't be able to determine the amount of constructs to discard that
way, so this should never be done unless you're absolutely sure. In this case,
we are absolutely sure that it will never be called with a
semicolon, because the mapping iterator has no use for the closure other than
the returned object (which is the case for most closures in practice.),
therefore we could even omit the return type declaration and get { | 5 *}
.
Neat!
We can use foreach
on iterators just like arrays. _str
is used to convert
a number to a string.
{ | _str println } swap:foreach
0
5
10
15
20
Ranges are inclusive of the lower bound and exclusive in the upper bound. They are often used similarly to the (pseudocode) equivalent in other languages:
for(int i = 0; i < 5; i++) { println((String) i * 5); }
SPL actually isn't fully concatenative. It supports postfix arguments as well:
println<"and with that, we're done">
This is actually not a special interpreter feature, more so is it a special
lexer feature. This is 100% equivalent with the non-postfix version, where the
string is right before the println
.
The same can be done for object calls. Let's rewrite the previous code with prefix notation:
Range:new<0 5>
:iter
:map<{ | 5 * }>
:foreach<{ | _str println }>
I lied. This is now no longer 100% equivalent. Let's look at what happens under the hood.
call Range
objpush
const mega 0
const mega 5
objpop
objcall new
objcall iter
objpush
const func 0
const mega 5
call *
end
objpop
objcall map
objpush
const func 0
call _str
call println
end
objpop
objcall foreach
You can see there are now objpush
and objpop
instructions. This is doing
the job that swap
used to do in our previous example. However, swap can only
swap the topmost values, but postfix arguments allow any amount. That's why
there is a special instruction just for that. It can also be used through AST
modifications, but there is no way to get it in normal language use as it can
cause interpreter panics when they are used wrongly.
objpush
and objpop
operate on a separate stack, called the objcall stack,
as opposed to the main object stack.
More of this tutorial to follow.
Because SPL does not nearly have a complete standard library, embedding rust is required for many tasks. This can be done as follows
> cat rust-test.spl
func main { |
1 rusty-test _str println
0
}
func rusty-test @rust !{
println!("hii");
let v = #pop:Mega#;
#push(v + 1)#;
}
> spl --build rust-test.spl demo
---snip---
> ./spl-demo/target/release/spl-demo rust-test.spl
hii
2
As you can see, it's relatively straight-forward to do; but there are some major limitations right now:
The second one is easy to fix, but I intend to fix the first one first. Sadly, fixing it requires compiling the code as a dynamic library and also getting it to work with the program its running in. If anyone knows how to do this properly, I'd REALLY appreciate a PR or issue explaining it.
There's a lot of things that look kinda tedious to do in SPL, syntactically speaking. So over time, I regularly add syntactic sugar:
a => b
-> a b match
a =>? b
-> a dup b match _'match-else-push
(match that will push the offending value on error)a =>! b
-> a b match _'match-else-error
(match that throws an error when unable to match)a<| b c d e>
-> a<{ | b c d e }>
-> { | b c d e } a
def a, b, c
-> def a def b def c
The match function match
and its accompanying sugar take in two values and essentially do a special
compare for equality on them. Unlike everything else, which is just compared normally, arrays are
iterated through and their items checked individually, recursively. When a callable ({ ... | ... }
)
is found in b, any value in a is accepted for it, and the function is called if and only if every
other part of the match also succeeds.
The function returns 1 on success, otherwise 0.
Example:
def a, val
[ ^ok "hey matcher" ] =a
a =>! [ ^ok &=val ]
val println
a =>? [ ^ok &=val ] not if {
"error: " swap concat panic
}
val println
a => [ ^ok &=val ] not if {
"error" panic
}
val println