Language Manual¶
IN PROGRESS!!!!
Spry code consists solely of comments, literals, words and composites separated by whitespace.
In Spry everything is an AST node, hereafter referred to as a node. The parsing step produces an AST (Abstract Syntax Tree) of such nodes from a string of code. This AST fully mirrors the Spry syntax. The nodes can then be evaluated. This means the Spry VM executes the AST directly without compiling it further to bytecodes or native code.
Comments¶
In Spry a comment begins with a #
parsed outside literals and consumes the rest of the line. This is the only syntax for comments.
# Comments can be on their
# own lines but each line must start with a #
echo "Hey" # But they can also begin after code
echo "Comments begin with # but they can not start inside literals"
Literals¶
Spry has three standard literals; int, float and string. A literal is a specific syntax that the parser can identify and solely from that - create the proper type of node. Te nodes created are "boxed" Nim values of the corresponding Nim types int
, float
and string
.
It is worth noting that the literal nodes are created during parsing and not during evaluation.
Note
The Spry VM also supports pluggable literals so you can easily add your own new literals in extra Spry VM modules,
there is an example in the module spryextend.nim
that adds support for multiline string literals.
Literal int¶
An int is signed and its size is platform dependent and has the same size as a pointer. _
is allowed inside an integer literal and is ignored, it can make big numbers easier to read like 340_000_000
. Examples are 42
, -34
, +12
.
Literal float¶
The size of a float is platform dependent, the Nim compiler chooses the processor's fastest floating point type. The literal syntax supports exponents using either e
or E
and also ignores any embedded _
. Examples are 3.14
, 4e2
, -2.734e-3
and 40.00_001e2
.
Literal string¶
A string is written using double quotes like "abc"
. Inside the string we replace any \\
by \
, any \'
by '
, any \"
by "
and any \xHH
(where HH
is a hexadecimal value) with the corresponding ASCII character. All characters are valid inside the string, including newlines. Examples are "abc"
, "hey \"there\""
and "abc\x0Adef"
.
Words¶
If it's not a literal, then it's a word. Words are separated from each other using whitespace or any of the composite characters "()[]{}
". There is also a special set of characters that can only form words together with other characters from the same set (or on their own) - ",;\^&%|~
". Of these only ,
, ;
and ^
are current core spry words, the other characters are still unused.
Note that a word in Spry can be a single character like =
or $
or a combination thereof, it doesn't have to be an alphabetic sequence. Words are normally keys bound to values and they are used both as classic "variables" but also for naming functions and other constructs in Spry. This is borrowed from Rebol.
Note
The special characters rule removes some uncomfortable need of whitespace to separate things but you still need whitespace to ensure that words like =
and +
are properly parsed for code like 3 + 4
or x = 14
. Pull requests to fix this in the parser are welcome :)
Words come in different types depending on prefixing characters. The word type dictates the behavior when the word is evaluated:
- A Get word is prefixed with
$
. - An Eval word has no prefix. This is the most common word.
- A Literal word is prefixed with
'
.
Get and Eval words also come in 5 different variants, totalling 11 different concrete word types.
Finally we also have Keywords which is simply a "syntactical sugar" in the Spry parser that enables Smalltalk style keyword calls.
Get word¶
When evaluated, a get word simply performs a lookup using the word itself as the key and the result is the node found. Get words are used when you want to refer to something by name and make sure you only perform the lookup. This way you can refer to for example functions without evaluating them.
Note
In many other languages you refer to the function itself using it's name like foo
and you call the function using a different syntax, for example foo()
.
In Spry it's the other way around, we call functions simply by their name like foo
, but if we want to refer to the function itself without calling it
we use $foo
.
There are different variants:
- Regular get word, for example
$x
. This looks first among local variables (inlocals
), then outwards lexically. - Module qualified get word, for example
$Foo::x
. This looks directly in the Module namedFoo
and nowhere else. It's equivalent toFoo at: 'x
. - Self get word, for example
$@x
. This looks directly inself
and nowhere else. It's equivalent to$self::x
orself at: 'x
. - Outer get word, for example
$..x
. This is like a regular get word but does not first look in local variables. - Arg get word, for example
:$x
. This pulls in the next argument for the lexically closest outerfunc
, without first evaluating it in the calling scope, and stores it inlocals
asx
. This makes it possible to write "macro like" functions.
Eval word¶
An eval word works like a get word, but it also evaluates the result of the lookup. If the result of the lookup is for example a func
then it is called. Just like get words we have the same 5 different variants, the difference is only the added evaluation.
- Regular eval word, for example
x
. This looks first inlocals
, then outwards lexically and then evaluates the result. - Module eval word, for example
Foo::x
. This looks directly in the Module namedFoo
and nowhere else, and then evaluates the result. It's equivalent toeval (Foo at: 'x)
. - Self eval word, for example
@x
. This looks directly inself
and nowhere else and then evaluates the result. It's equivalent toself::x
orself at: 'x
. - Outer eval word, for example
..x
. This is like a regular eval word but does not first look inlocals
. - Arg eval word, for example
:x
. This pulls in the next argument to the lexically closest outerfunc
, but first evaluates it in the calling scope, and then stores the result inlocals
asx
.
Literal word¶
A literal word is a canonicalized (there is only ever one instance of every unique word) string and is written like 'foo
. This is very much like a Symbol in Smalltalk or Ruby. Obviously all described words above can be created in literal form just by prepending a '
, like '$foo
, ':foo
and so on. A literal word evaluates as itself.
You use a literal word when you want to compose code or in situations where you would use a Symbol in Ruby/Smalltalk. Literal words can be reified using reify
.
Keywords¶
The message syntax of Smalltalk has proven itself over many years to be very readable and expressive. In Smalltalk a call (message) that takes arguments is written in "keyword style" like 'hello' copyFrom: 1 to: 2
which roughly in other languages would translate to "hello".copy(1, 2)
or at best "hello".copy(from=1, to=2)
. The advantage of the Smalltalk style is that it doesn't make you wonder what the arguments actually denote and in which order they should be given.
The name of the method in this example in Smalltalk is copyFrom:to:
so the colons are part of the identifier. Spry (unlike Rebol) allows for colons in words just like Smalltalk does and the Spry parser will thus automatically perform a rewrite of "hello" copyFrom: 1 to: 2
into "hello" copyFrom:to: 1 2
thus turning the call into a regular infix (first argument to the left) Spry function call, see Functions.
Both ways of writing are valid, although the latter would be... very ugly.
But before explaining functions and function calls we first need to go through some simpler parts.
Precedence¶
All programming languages have more or less complicated rules for evaluation precedence in expressions. Rebol and Smalltalk excel with very simple rules, while some other languages have dreadful intricate rules. Rebol and Smalltalk both have a base rule of evaluating from left to right and Spry has the very same rule.
Then Smalltalk evaluate unary before binary, and binary before keyword messages. This usually removes a lot of parentheses and is quite natural in Smalltalk. Rebol has something similar where it evaluates operators before functions.
But in Spry there is no such additional rules. The rule is simply from left to right. To do anything different you use parentheses!
x = (3 + 4) # Otherwise Spry assigns only 3 to x
y = (2 + 3 * 4) # Equals 20
y = (2 + (3 * 4)) # Equals 14
Booleans¶
There are no literals for booleans (!), but there are two singleton instances of the VM's internal boolean node type (BoolVal) that are bound to the words true
and false
in the root namespace. That's a mouthful explaining that yes, you can use true
and false
pretty much as usual. This design is borrowed from Smalltalk.
x = true
y = false
x and y then: [echo "Both are not true"]
x or y then: [echo "But one is true"]
y not then: [echo "Y is not true"]
y else: [echo "Y is not true"]
Nil and Undef¶
In the same manner as booleans we also have the word nil
bound to the sole instance of the NilVal node type, and undef
bound to the sole instance of UndefVal.
Spry is experimenting with distinguishing between nil
meaning nothing and undef
meaning never assigned any value. For any word the value nil
is thus a valid value. When a lookup doesn't find any valye for the key and fails you will get undef
. If you get nil
it means the lookup succeeded and the value in question is in fact nil
. There is a primitive method called ?
that checks if a node is undef
.
echo x # prints "undef"
echo (x ?) # prints "false"
x = nil
echo (x ?) # prints "true"
echo x # prints "nil"
Composites¶
Literals and words are the "atoms" of Spry but in order to compose both data and code (Spry is homoiconic) we need some kind of "structures of many". We have three of these in core Spry - Block, Paren and Curly.
Block¶
The Block is the work horse sequential structure in Spry but it is also the unit of code! Compared to Smalltalk one can almost say it does double duty as BlockClosure and OrderedCollection. A Block is formed using []
, for example [1 + 2]
. There is no separator needed.
When parsed this will create a Block and when evaluated a Block evaluates to itself - in other words, it does nothing.
But if we have a Block we can do a lot of things with it explicitly - like evaluating it as code using the do
function:
# Evaluates to 3, try it in ispry
do [1 + 2]
A Block evaluates in its own local closure which means that the activation of the block has its own Map of local bindings. Assignments operate on this Map but it can also be explicitly accessed using the locals
primitive. But in Spry we can also manipulate the Block as a sequence:
# Evaluates to 7
foo = [1 + 2]
foo at: 0 put: 5
do foo
Note
Yes, in Spry we use the same convention as in Nim regarding indexing - first element is at position 0.
Paren¶
In most programming languages parentheses are only used to control evaluation order and do not reify as anything concrete during execution. In Rebol and Spry however, (1 + 2)
reify as a Paren when parsed. A Paren is a Composite internally just like a Block is, but when evaluated a Paren evaluates itself as code without creating a new closure and the result is the result of the last expression. It can be used pretty much as parentheses are used traditionally.
Curly¶
The third Composite is the Curly which is written like {a b c}
. It reifies as a Curly when parsed, which also is a Composite internally just like a Block. When a Curly is evaluated it evaluates itself like a Block does in its own closure, but the result is the locals Map of the closure. The net effect of that is that we use Curlys to create Maps:
map = {x = 50 y = 100}
Map¶
As mentioned, the Block is the sequential collection in Spry corresponding to OrderedCollection in Smalltalk. The Map corresponds similarly to Dictionary. All nodes in Spry can be used as keys in a Map. Word nodes come in 11 different types, as earlier described, but when used as keys only the actual word itself is used for hash and equality. This means that when used as keys, $foo
, foo
and :foo
(for example) are all equal since only the actual word "foo" is used for hash and ==
. Literal words on the other hand are always different from each other, for example '$foo
is different from 'foo
.
Root¶
Spry is similar to Smalltalk in the sense that there is a special Map that holds all globals. In Smalltalk that Dictionary is called Smalltalk
, in Spry we call it root
.
The root Map is created by the Spry interpreter and populated with words referring to primitive functions and known values like true
, false
etc. Lookups are done through the lexical scopes, which Blocks and Curlys create when evaluated, up to root.
If a lookup still fails there is a special Block held in the word modules
containing all Modules that we want should participate in global lookups, in the order they are in that block. A Module is just a Map with some extra meta information in it.
The interpreter will iterate through the Modules and perform the lookup in each one until finding a hit, or giving up and returning undef
. This means Modules will shadow each other depending on their position in the modules
block. But you can always refer to names directly using Module getwords Foo::x
or simple Map lookups like Foo at: 'x
.
This design is an experiment in "modelessness" since there are no import statements or other mechanisms to modify how lookups are made in a certain part of your source code. At the same time Modules can be loaded and used even if they do contain naming conflicts.
Functions and Methods¶
This inevitably brings us to functions, or Funcs as they are called in Spry. Spry is a heavily functional language, in fact, there are no builtin keywords or operators at all in Spry - everything is a function, including such fundamental things as assignment =
.
Funcs can be written either as "primitives" in Nim or in Spry of course. The Spry VM has a bunch of Funcs included internally, most of which are primitives but also some defined in Spry.
Then there are VM modules that define more Funcs, again both primitives and Spry funcs. VM Modules are linked into the VM when you build the Spry VM, currently statically but they could be made to load dynamically as .so/.dlls too.
Finally Spry also has Modules that are pure Spry.
Func¶
Functions are created from Blocks using the func
function that takes a Block of code, performs a shallow copy of it to create a Func and returns it:
# We create a func from a block and assign it to foo
foo = func [3 + 4]
# Now evaluating foo will give 7
foo
A Func evaluates the block when it's evaluated. This is in contrast to a Block which evaluates to itself. The return value of a Func is either the result of the last expression or you can return explicitly using the primitive return Func using the same character as in Smalltalk, ^
. Another important aspect of Funcs is that they are not polymorphic, or in other words, you can not overload them for different types of the arguments. However, several of the builtin core Funcs perform a bit of "type testing" internally so that you can indeed call them with different types of arguments and they handle them properly. For true polymorphic behaviors you should use (Polymethods](#Polymethods) in Spry.
Funcs use prefix calling and they are called just like in Rebol:
# Call foo with no arguments
foo
# Call foo with one argument, an int
foo 4
# Call foo with two arguments
foo 4 "hey"
Arguments to Funcs are "pulled in" using Arg words which are prefixed with :
. Note that an arg word is an operation, not a declaration, so they can appear anywhere in the Func.
# This func takes one argument and adds 4 to it
foo = func [:x + 4]
# Prints 9 on stdout
echo foo 5
The Func echo
is included in the VMModule spryio
which in turn is included in the standard spry and ispry VMs. For embedded use one can however build a Spry VM that does not include it. echo
is a prefix Func that takes one argument that it will turn into a string before writing it to stdout using Nim's echo proc.
The arg word :x
will take the next argument AST node (the 5
literal) to the foo
Func, evaluate it at the call site (literals evaluate to themselves) and take the resulting node and store it in the local variable x
in the Func closure, and the result of the arg word will be that node. Since the result is the value it means you can actually use :x
in an expression just like we do above.
Note
The fact that arg words are operations also means the arity of a Func is not static, it could in theory pull in a different number of arguments - although that would be very confusing.
You could also write the Func in "Smalltalk style" that looks like declarations:
# This func takes one argument and adds 4 to it
foo = func [:x x + 4]
# Prints 9 on stdout
echo foo 5
Currently that will be slightly slower since we first evaluate :x
and then x
but it may sometimes make the code easier to read.
Arg words can also be "get arg words" which means that we can pull in the argument AST node without first evaluating it at the call site. Here is an example that shows the difference:
foo = func [:$x echo $x]
bar = func [:x echo $x]
x = "abc"
bar x # prints "abc"
foo x # prints "x"
bar (3 + 4) # prints "7"
foo (3 + 4) # prints "(3 + 4)"
The reason we use echo $x
is to prevent x
from being evaluated inside the func.
Methods¶
Methods are just like Funcs but they always take at least one argument, from the left. This mandatory "receiver" is accessible using the primitive func self
, so no need to use an arg word to pull it in. This means Methods "feel" like OO messages, but they are still not polymorphic based on the receiver, again you should use Polymethods for that.
# Call method foo on an int
4 foo
# Call method foo on a string, with one more argument
"hey" foo 7
# Call method foo with three arguments
4 foo "hey" "there"
Methods are created using method
:
# Create a method that adds 5 to self
plusfive = method [self + 5]
echo (3 plusfive) # prints "8"
Both methods and funcs can use keyword naming and calling style:
# Create a function and assign it to a keyword
add:to: = func [:x + :y]
echo (add: 5 to: 6) # prints "11"
# And a method in the same way
add:and: = method [self + :x + :y]
echo (3 add: 5 and: 6) # prints "14"
# Can also be called like this
echo (add:to: 5 6) # prints "11"
echo (3 add:and: 5 6) # prints "14"
Scoping¶
Spry code is organized in nested Blocks. Each Block is a scope, a closure in fact. And each closure has a Map containing its local bindings, which you can reach explicitly using locals
. The top level's local bindings are in fact root
.
Words are what you use to refer to things in Spry so if you use a regular eval word like foo
, that means lookup the key "foo" first in the locals and then outwards until reaching the global scope. Then evaluate whatever value is returned from the lookup. In Spry we don't declare anything, not even local variables like you do in Smalltalk, instead we have 5 different variants of words to reach our bindings, here are all 5 described once more:
# Lookup in locals and outwards to root and all Modules listed in modules, undef if not found
foo
# Lookup outside this closure and outwards to root and all Modules listed in modules, undef if not found
..foo
# Lookup in the Map called Bar, undef if not found
Bar::foo
# Lookup in self which is the nearest receiver Map
@foo
# Pull in the next argument to this Block invocation
:foo
The first 4 variants can also be used as left side in an assignment with these meanings:
# Bind in locals, regardless of any outer reachable foo's
foo = 5
# Lookup outside this closure and outwards to root and all Modules listed in modules.
# If found assign to that foo, otherwise bind in nearest outer closure.
..foo = 5
# Bind in the Map called Bar
Bar::foo = 5
# Bind in self which is the nearest receiver Map
@foo = 5
The most uncommon effect of these rules is that you often need to use ..foo
as left hand side in assignments being done inside control structure blocks. This is because all blocks are closures and we don't declare locals in Spry so Spry has no way of knowing that you want to assign to the outer foo
and not a local foo
. This means that the following code has to be rewritten to work as intended:
foo = func [ :a
x = 10
a > 10 then: [x = 20] # This needs to say "..x = 20"
^x]
echo foo 5 # prints 10
echo foo 12 # still prints 10!
The reason is that x = 20
sets x
in the local then-block, not in the outer func block. Rewriting with ..x
solves it, but we can perhaps do this instead:
foo = func [ :a
x = (a > 10 then: [20] else: [10])
^x]
echo foo 5 # prints 10
echo foo 12 # prints 20
Or even shorter of course:
foo = func [:a > 10 then: [20] else: [10]]
echo foo 5 # prints 10
echo foo 12 # prints 20
For the moment this is a "language wart" - in other words - something I would like to fix but not sure exactly how yet. :)
Note
The rules for the left side in assignments are under evaluation. A variant could be that we distinguish between func/method/curly scopes and other blocks.
Standard Library¶
The Spry VM includes a very minimal "standard library" in the form of primitive Methods and Funcs, a few Spry only Methods and Funcs and a few singleton nodes. The VM creates the global root
Map and populates it with words.
The regular Spry VM spry
and the REPL ispry
also loads several VM Modules that adds more words to root
. Finally, the file spry.sy
is loaded with additional Spry library code that are not primitives.
Singletons¶
The following singleton nodes are created and bound to these words by the VM.
Word | Comment |
---|---|
false |
Reference to the singleton for falsehood |
true |
Reference to the singleton for truth |
undef |
Reference to the singleton for missing value |
nil |
Reference to the singleton for value meaning no value |
modules |
Reference to a Block of Maps for lookups |
Reflection¶
The following Funcs and Methods are available for reflection purposes.
Word | Type | Comment |
---|---|---|
root |
Func | Returns the Map of global bindings |
activation |
Func | Returns the current Activation |
locals |
Func | Returns the Map of local bindings |
self |
Func | Returns the receiver in a Method, undef in a Func |
node |
Func | Returns the receiver in a Method, unevaluated |
; |
Func | Returns the previous receiver, enables Smalltalk style cascades |
type |
Method | Returns a literal word representing the nodetype, see below. |
The method type
returns a literal word representing the type of the receiver node: 'int
, 'float
, 'string
,'boolean
, 'undefined
, 'novalue
, 'block
, 'paren
, 'curly
, 'map
, 'binding
, 'evalword
, 'evalmoduleword
, 'evalselfword
, 'evalouterword
, 'evalargword
, 'getword
, 'getmoduleword
, 'getselfword
, 'getouterword
, 'getargword
, 'litword
Note
The Activation node does not yet expose any functionality, but it will eventually be used to open up access to the execution stack etc similar to thisContext
in Smalltalk. The reason for root
to not be a reference to the singleton is to avoid a recursive global Map.
Creating things¶
Spry is not a class based language. Things are created either using literal syntax (created at parse time), through specific evaluation mechanisms (maps are created through evaluating curlys) or through cloning already existing things:
# Literal syntax, created at parse time
x = "abc"
y = 12.0
z = 99
# Composites, created at parse time
# $ prevents evaluation so that we can hold the paren/curly itself
paren = $ (1 + 3)
curly = $ {1 2 3}
# No need for $ since blocks evaluate to themselves
block = [1 2 3]
# Create a map through evaluating a curly
# The curly is created at parse time, but the map
# is created when we evaluate the curly
map = {x = 12}
# Cloning at evaluation time, note need for parens
x = ("abc" clone)
y = ([1 2 3] clone)
z = ({x = 12} clone)
Word | Type | Comment |
---|---|---|
clone |
Method | Performs a copy of strings, does nothing for floats and ints and a performs a shallow copy of blocks, parens, curlys and maps. |
Tags¶
All nodes of all types in Spry can have a block of tags. Tags are currently limited to being literal words. It's used mainly for Maps and Polymethods which forms the basis of OO in Spry.
Word | Type | Comment |
---|---|---|
x tag: aWord |
Method | Add a tag on a node x. If aWord is not a literal word, it will be converted to one |
x tag? aWord |
Method | Check if a node x has the given tag. If aWord is not a literal word, it will be converted to one |
x tags |
Method | Returns the Block of tags on node x |
x tags: aBlock |
Method | Set the Block of tags on node x |
Assignments¶
In Spry we don't have Rebol style "set words", instead we have a word =
that is bound to a primitive Method that performs assignment. This method uses the left hand side unevaluated, which means it works for most normal cases. For more advanced cases where you want to compute the left hand side you can use set:
instead. Similarly ?
has a counterpart in get?
.
Finally there is no unset word, but you can instead assign to undef
which will remove the binding.
Currently there is a language wart that usually forces assignments to be written with parentheses. Spry has no statement separators and does not consider line breaks. You can write a Spry program in one single long line. This means Spry has no simple of knowing when the expression ends. Due to this the current rule is that assignment only consumes a single node from the right.
# You need a paren here because otherwise
# Spry finds "3" and evaluates it (to 3 obviously)
# and then assigns that to x.
x = (3 + 4)
# This works because readFile is a func and when
# evaluated it will pull in the argument filename
x = readFile "afile.txt"
One idea to improve this without introducing separators or making line breaks meaningful (I don't want to do any of those two) is to make evaluation more "eager". Using "look ahead" Spry could check if the word coming after "3" is in fact a method, and then it would continue evaluating it.
Word | Type | Comment |
---|---|---|
x = aNode |
Method | Assigns the right hand node to the left hand word x. Left hand side is not evaluated. It can be a literal word or a regular word for local binding. An outer word for binding to an outer closure, or a module word for binding in a Map or Module. |
x ? |
Method | Checks if x is bound to something. Left hand side is not evaluated. It can be any kind of word, including a literal word. |
x set: aNode |
Method | Assigns the right hand node to the left hand literal word. Left hand side is evaluated and should evaluate to a word which is used through the same rules as = . |
x set? |
Method | Checks if x is bound to something. Left hand side is evaluated and should evaluate to a word. |
Arithmetic¶
Spry has int and float as numeric nodes and will automatically convert from int to float if they are mixed.
Word | Type | Comment |
---|---|---|
+ , - , * , / |
Method | Normal arithmetic methods, ints are converted to floats if needed |
Comparisons¶
Word | Type | Comment |
---|---|---|
< , > , <= , >= |
Method | Defined so far for int, float and strings |
Equality¶
In Spry =
is used for assignment so we use ==
(and !=
) for testing equality and ===
for testing identity.
Word | Type | Comment |
---|---|---|
== |
Method | Check equality |
=== |
Method | Check identity |
!= |
Method | Checks for inequality |
!=== |
Method | Checks for non identity |
Booleans¶
Word | Type | Comment |
---|---|---|
not |
Method | Negates a boolean |
and |
Method | If left hand side expression evaluates to true, then right hand side is also evaluated. True if both are true, otherwise false |
or |
Method | If left hand side expression evaluates to false, then right hand side is also evaluated. True if either is true, otherwise false |
Concatenation¶
Word | Type | Comment |
---|---|---|
, |
Method | Concatenates strings, blocks, parens and curlys |
Conversions¶
Word | Type | Comment |
---|---|---|
print |
Method | Returns the node in the user friendly string format for presentation (like Rebol's "form") |
parse |
Func | Parse a string of Spry into nodes, comments are kept |
serialize |
Method | Returns the node as a string in source form excluding comments |
commented |
Method | Returns the node as a string in source form including comments |
asFloat |
Method | Converts an int to float |
asInt |
Method | Converts a float to int |
Composites¶
These methods operate mainly on the sequential composites - blocks, parens and curlys. Some also work for strings. And some also work for maps.
Word | Type | Comment |
---|---|---|
size |
Method | Returns number of elements, works for string, blocks, parens, curlys and maps. |
at: |
Method | Get element at a key, works for blocks, parens, curlys and maps. Returns undef if not found. |
at:put: |
Method | Set element at a key to a value, works for blocks, parens, curlys and maps. Using undef as value means removing the binding for Maps. |
get: |
Method | Get element at unevaluated argument. Works for blocks, parens, curlys and maps. Returns undef if not found. |
set:to: |
Method | Set element at unevaluated argument. Works for blocks, parens, curlys and maps. Using undef as value means removing the binding for Maps. |
add: |
Method | Add element to sequential composite. Works for blocks, parens and curlys. |
removeLast |
Method | Remove last element of sequential composite. Works for blocks, parens and curlys. |
copyFrom:to: |
Method | Copy a sub range to form a new composite of the same type. Works for blocks, parens and curlys. |
contains: |
Method | Test if the composite contains any element equal to the given argument. Works for blocks, parens, curlys and maps. |
first |
Method | Return element at position 0. |
second |
Method | Return element at position 1. |
third |
Method | Return element at position 2. |
fourth |
Method | Return element at position 3. |
fifth |
Method | Return element at position 4. |
last |
Method | Return last element. |
do: |
Method | Iterate over block, paren or curly and evaluate argument block for each element, Smalltalk style. |
sum |
Method | Sums all elements of a block, paren or curly. Can contain a mix of ints and floats. If all are int result will be an int. |
For Maps you can also use ::
-syntax to get and together with =
to set. Using undef
as value means removing the binding for Maps.
Blocks¶
Blocks actually double as positionable streams too, with an internal position just like in Rebol. This means we can easily step through a Block using its internal position. The following methods form the base of this stream protocol.
Word | Type | Comment |
---|---|---|
reset |
Method | Set position to 0 |
pos |
Method | Get current position, first position is 0 |
pos: |
Method | Set current position |
read |
Method | Get the element at the current position without moving forward |
write: |
Method | Set the element at the current position without moving forward |
next |
Method | Get the element at the current position and increase the position |
prev |
Method | Get the element at the current position and decrease the position |
end? |
Method | Return true if position is >= size, which indicates we have reached the end |
Funcs and Methods¶
The core unit of behavior in Spry is funcs and methods. As described above, both work essentially the same, but methods take the first argument (the "receiver") from the left and make it accessible using self
, you don't use an arg word to pull it in. You use ^
to do an early return, just like in Smalltalk.
Word | Type | Comment |
---|---|---|
func |
Func | Creates a func which is a prefix function taking all arguments on the right side |
method |
Func | Creates a method which is an infix function taking the first argument, the receiver, on the left side |
Evaluation¶
The following funcs performs explicit evaluation in various ways.
Word | Type | Comment |
---|---|---|
do |
Func | Takes one argument, a block. Evaluates the block. |
$ |
Func | Takes one argument but does not evaluate it, returns it unevaluated. Can be used to prevent evaluation of nodes. |
eva |
Func | Takes one argument, evaluates it and returns the result. |
eval |
Func | Takes one argument, evaluates it and then also evaluates the result. |
^ |
Func | Takes one argument, evaluates it and performs an early return. |
Words¶
Spry has 11 different kinds of Words. The following funcs can create and convert words in different ways.
Word | Type | Comment |
---|---|---|
reify |
Func | Makes a word from a literal word |
litify |
Func | Makes a literal word from a word |
quote |
Func | Makes a literal word from an unevaluated word |
litword |
Func | Makes a literal word from a string |
word |
Func | Makes a word from a string |
Conditionals¶
Spry uses Smalltalk style keyword based conditionals but I decided to rename the Smalltalk variants ifTrue:
, ifFalse:
, ifTrue:ifFalse:
, ifFalse:ifTrue:
to the slightly shorter then:
, else:
, then:else:
, else:then:
.
Word | Type | Comment |
---|---|---|
then: |
Method | If receiver is true then evaluate the given block |
else: |
Method | If receiver is false then evaluate the given block |
then:else: |
Method | If receiver is true then evaluate the first block, otherwise the second |
else:then: |
Method | If receiver is false then evaluate the first block, otherwise the second |
Other variants can easily be implemented in Spry too, like for example implementing ifTrue:
, using then:
. Pointless of course:
ifTrue: = method [:blk self then: [^do blk] nil]
3 < 4 ifTrue: [echo "Works"]
Or you could of course just make an alias, assigning ifTrue:
to the same primitive as then:
is bound to.
ifTrue: = $then:
3 < 4 ifTrue: [echo "Works"]
Loops¶
The following loop words are also designed like in Smalltalk.
Word | Type | Comment |
---|---|---|
timesRepeat: |
Method | Smalltalk style loop. The receiver is an int and the argument is a block to evaluate that number of times |
to:do: |
Method | Smalltalk style for-loop, although currently limited to a step by +1 |
whileTrue: |
Method | Smalltalk style conditional loop |
whileFalse: |
Method | Smalltalk style conditional loop |
Misc¶
Word | Type | Comment |
---|---|---|
quit |
Func | Quits the interpreter. Takes one argument, the numeric exit code to return to the OS |
Polymethod¶
Spry library¶
Word | Type | Comment |
---|---|---|
error | ||
assert | ||
obect | ||
module | ||
sprydo: | ||
detect: | ||
select: |
Modules¶
Modules in Spry are simply Maps with an additional entry under the key _meta
with a Map containing the meta information about the Module. Here is an example:
{
_meta = {
name = 'Foo
version = "1.0"
description = "Testing module closure"
}
# Local baz. The Map itself is the lexical parent of the funcs we create below.
# That means that lookups inside the funcs will continue outwards through the Map.
baz = 1
# Here we rely on a baz in this module, or else in the global scope
bar = func [:x + baz]
# Here we make sure to use the module baz, avoiding the local one inside the func
bar2 = func [ baz = 99 :x + ..baz]
# Here we use Foo::baz, which will resolve to 1 if this module is indeed loaded as Foo
bar3 = func [:x + Foo::baz]
}
As we can see the above is just Spry syntax for a Map and the Map contains 3 funcs and one "variable" called baz
. If we put this in foo.sy
- the filename is not important and can be anything - we can then load this module into Spry using loadFile: "foo.sy"
. The default behavior is to load and bind the module to the name in _meta
as a global in root
. Then we can manipulate it and reach it's members using Foo::xxx
syntax:
# Load the module as the name it has in the neta information
loadFile: "foo.sy"
# We can now access stuff in Foo, should print 1
echo Foo::baz
# Run the bar func with 1 as argument, should print 2
echo Foo::bar 1
# Set a global value for baz
baz = 10
# This should return 2
echo Foo::bar2 1
# And this should return 2
echo Foo::bar3 1
# If we throw away Foo and load it as Zoo
Foo = undef
loadFile: "foo.sy" as: 'Zoo
# Then all works the same, should print 2
echo Zoo::bar 1
# Create a different Foo, so that bar3 finds something
Foo = {baz = 8}
# Should print 9
echo Zoo::bar3 1
Finally, we can also add modules to the special block called modules
which is used by the lookup machinery in Spry. Lookups go outwards lexically all the way up to root
and if it fails Spry then looks in each module listed in modules
until giving up and returning undef
.
VM Modules¶
Core Spry comes with a bunch of assorted VM modules. A VM module is a separate Nim package that has a Nim proc that "adds it" to a Spry Interpreter instance. The idea is that when you build a Spry VM you pick which VM modules you want to include and then call them one by one. The standard Spry VM has a section that looks something like this:
import spryextend, sprymath, spryos, spryio, sprythread,
spryoo, sprydebug, sprycompress, sprystring, sprymodules,
spryreflect, spryui
var spry = newInterpreter()
# Add extra modules
spry.addExtend()
spry.addMath()
spry.addOS()
spry.addIO()
spry.addThread()
spry.addOO()
spry.addDebug()
spry.addCompress()
spry.addString()
spry.addModules()
spry.addReflect()
spry.addUI()
Here we see that the regular VM imports a bunch of VM modules at the top, and then calls addXXX
for each one. Let's look closer at the LZ4 compression VM module called sprycompress.nim
:
import lz4
import spryvm
# Spry compression
proc addCompress*(spry: Interpreter) =
# Compression of string
nimFunc("compress"):
newValue(compress(StringVal(evalArg(spry)).value, level=1))
nimFunc("uncompress"):
newValue(uncompress(StringVal(evalArg(spry)).value))
The name addXXX
is just convention, but it must take an argument spry of type aInterpreter
. Then in that proc we can do several things, but perhaps most importantly we can add primitives to Spry. We typically do that using the Nim templates nimFunc
and nimMeth
. A primitive is given a name and the code has access to the Interpreter via spry
. Using Nim procs like evalArg(spry)
we can pull in the next argument (evalArgInfix(spry)
pulls in the receiver from the left) and at the end the primitive must return a Node
. newValue
will create the proper Node from a bunch of Nim types.
The templates nimFunc
and nimMeth
will then create a NimFunc (or NimMeth) node and bind it to the name given in the Spry root
Map.
By looking at the various VM modules you can easily see how to make your own! It's easy.
Spry grammar¶
Here is a very informal extended BNF of Spry (as of now) written using similar conventions that Nim does. I think it's fairly correct - however - the current parser in Spry is a handwritten iterative (not recursive) parser so some parts were hard to express, like rules for comments and whitespace, see the notes in the BNF for details.
In this EBNF (a)*
means 0 or more a's, a+
means 1 or more a's, and (a)?
means an optional a. Parentheses may be used to group elements.
{}
are used to describe character sets. Stuff I can't figure out is described inside {{ }}
using plain english.
Note that true, false, nil, undef
and all control structures are expressed using words, so these 26 lines are actually the complete grammar!
# Ints are parsed using Nim parseInt, floats using parseFloat and strings using unescape.
# This means they follow the following grammar.
int = ['+' | '-'] digit (['_'] digit)*
exponent = ('e' | 'E') ['+' | '-'] digit (['_'] digit)*
float = ['+' | '-'] digit (['_'] digit)* (('.' (['_'] digit)* [exponent]) | exponent)
# Inside the string we replace any \\ by \, any \' by ', any \" by " and any
# \xHH (where HH is a hexadecimal value) with the corresponding character
string = '"' {{all sequences of characters except a \" not preceded by a \\}} '"'
# Literals are pluggable in the Parser, but these three are the core ones
literal = int | float | string
# Same definition as in Nim strutils, whitespace separates words in Spry
whitespace = {' ', '\x09', '\x0B', '\x0D', '\x0A', '\x0C'}
# Note that there is a set of special characters that can only form
# names together with other special characters.
name = {{any sequence of characters not parsed as a literal}}
qualifiedname = name '::' name
evalnormalword = (name | qualifiedname)
evalselfword = '@' name
evalouterword = '..' name
evalword = evalouterword | evalselfword | evalnormalword
getnormalword = '$' (name | qualifiedname)
getselfword = '$@' name
getouterword = '$..' name
getword = getouterword | getselfword | getnormalword
argevalword = ':' name
arggetword = ':$' name
argword = argevalword | arggetword
word = evalword | getword | argword
block = '[' program ']'
paren = '(' program ')'
curly = '{' program '}'
# Comments are detected outside literals and consume the rest of the line
# They do NOT constitute nodes in Spry. Yet.
comment = '#' (any)* '\l'
node = literal | word | block | paren | curly
# A program is just a sequence of nodes separated by optional whitespace.
# Perhaps not exactly correct, two words in sequence *must* be separated by whitespace.
program = ((whitespace)? node (whitespace)?)*