We have what looks like a working tokenizer and parser and you can evaluate most expressions that don't call or define a function (so nothing really useful is possible yet).
You'll need CMake and a C11 compatible C compiler, tested with gcc 11.1 on x86-64 Linux. Since only the C standard library is used, it should work pretty much anywhere, though.
- functions - Can be called with values as arguments and returns a value
### Comments
A `#` starts a comment that runs to the end of the line. Comments are not evaluated.
### Variables
A variable holds a value.
Variable names can be composed of all printable ASCII characters + bytes >= 0x80 (this allows for UTF-8 encoded variable names) excluding these reserved characters: `()[]{}~.@;,?#:"'\\`. Furthermore, variable names can not ...
- ... start with a digit
- ... contain the sequence `->`
- ... contain only the `=` character
- ... contain the `=` character after an alphanumeric character
The last two restrictions are for allowing identifiers like `==`, `!=`, `<=`, while still using `=` as an assignment operator.
The variable `_` is special: it can always be assigned to but will never actually store the value. This can be useful when deconstructing a value in a function or assignment (more on that later).
A string is surrounded by `"` quotes and can contain arbitrary bytes, with the exception of `"` (terminates the string) and `\` (is used for escaping).
-`\x...`: A byte of the hexadecimal value of the next two hexadecimal characters
Also, a `'` followed by a variable name evaluates to a string with the variable name as it's content. `'foo-bar` and `"foo-bar"` are equivalent.
### Lists
A list is created by `[]` with optional expressions inside the brackets. The expressions can be separated by commas (`,`), newlines, semicolons (`;`) or simply whitespace.
[] # An empty list
[1 2 3] # A list with 3 numbers
[1, 2,3] # The same list
[1
2
3] # The same list again
[[]] # Lists can be nested
When creating a list, you can also expand another list into the list with the `~` prefix:
As with lists, the comma (`,`) separator is optional and can also be replaced by newlines, semicolons. It's recommended to use commas when the dict is in one line and only line breaks if it spans multiple lines.
It is an error to mix key-value pairs and list items in an `[]` bracket pair:
[
"foo" -> "bar"
1 # This will fail, as a k -> v pair is expected
]
Individual values can be accessed using the `dict@key` syntax, where the `key` can be any expression. You can also use `dict.name`, which accesses the string key `name` (so it's equivalent to `dict@'name` or `dict@"name"`)
A symbol is a value, that is unique. It can only ne used for comparison and will only compare equal with itself. A symbol can optionally have a name that will be shown when printing or on the REPL to aid during development but can't otherwise be accessed.
A symbol can be created by calling the built-in function `symbol`, optionally with a name:
Anon1 := (symbol) # A symbol without a name
Anon2 := (symbol) # Another symbol without a namem distinct from Anon1
Foo1 := symbol 'Foo # A symbol with the name "Foo"
Foo2 := symbol 'Foo # Another symbol with the name "Foo", distinct from Foo2, even though they have the same name
Functions are the fundamental building block of apfl programs, as they are the only way of grouping code together and also the only way of control flow, as there are no traditional keywords like `if`, `while` and so forth (these are instead implemented as functions).
Functions are called by writing an expression that evaluates to a function (usually a variable), followed by the arguments:
some-function foo bar baz
The arguments are passed by value (conceptually a copy is created, although the runtime can decide to pass a reference to avoid costly copying, if it knows this won't violate the assumption that arguments inside the function are independent of their outside counterpart).
If a function should be called where an expression is expected, wrap it in parenthesis `()`:
some-function (some-other-function 1 2) # This calls some function with the result of some-other-function being called with 1 and 2
This also allows for a function to be called with no arguments:
foo # The value of the variable foo
(foo) # The function foo being called with no arguments
foo bar # The function foo being called with bar
(foo bar) # Again, the function foo being called with bar
((foo bar)) # The result of the function call "foo bar" called with no arguments
The argument part of a function call behave like the content of a list. This means you can also expand another list into the arguments with `~`:
Functions can be defined as simple functions or complex functions:
#### Simple functions
A simple function is created by wrapping expressions in `{}` curly braces:
foo = {
print "Hello World"
}
(foo) # Will call the function foo and therefore print "Hello World"
The last expression in the function body will be the return value:
foo = {
"Hello"
"from"
"foo" # Will be returned, as it's the last expression
}
print (foo) # Prints "foo"
Simple functions accept any number of arguments, but will discard them.
Functions have their own variable scope. Variables that are created within them are not accessible from the outside.
func = {
foo = "bar"
}
(func)
print foo # Error: Variable "foo" does not exist here
They can however access (both read and write) variables from outer scopes:
x = 1
foo = {
print x
x = 2
}
print x # Output: 1
(foo) # Output: 1
print x # Output: 2
If you want to use a variable name from outside the function inside without overwriting the variable, you can use `:=` instead of `=` to create a new local variable:
x = 1
foo = {
x := 2 # Note the `:=`, this creates a new local x.
print x
}
print x # Output: 1
(foo) # Output: 2
print x # Output: 1
#### Complex functions
Like a simple function, a complex function is written inside `{}` curly braces. A complex function contains subfunctions that are introduced by a parameter list. In the simplest case, a parameter list is a list of variable names, followed by an `->` arrow. The arguments the function is called with is matched against these parameters and the argument values will be available (as a copy) as the variable names in the parameters.
A function can contain multiple subfunctions, each with their own parameter list and their own body (everything after the parameter list up to the next parameter list or the end of the function). When being called, the first subfunction, which parameter list matches the arguments is getting evaluated. This can for example be used to provide a default argument:
A parameter can not only be a variable name, it can also be a constant value that the input is being matched against. As an example, here is a recursive definition of the factorial function:
It is often useful to deconstruct a pair and check the left value against a predefined value. If it's a constant value like a number, `::` can be used:
foo := {
42 :: x ->
+ x 1
}
However, sometimes the left value is not a constant. We could use predicates for this (see below), but we also have another syntax at hand for this:
A parameter list can also contain up to one expansion, a variable name preceded by `~`. All remaining arguments are copied into the variable as a list.
print-many "foo" "bar" "baz" # Prints "foo", "bar" and "baz" individually
Parameters can also be tested further by providing a predicate: A parameter followed by `?` plus an expression will result in a call of the expression with the argument for that parameter. If the call returns true, the argument is accepted.
As a final piece, a parameter can also be a list parameter: Parameters surrounded by `[]` brackets form a single parameter that matches against a list argument. The list is then matched against all parameters inside the brackets. All parameter types can be used here. As an example, here is a recursive implementation of the `map` operation. It creates a new list from an input list by applying a function to all values:
The left hand side of an assignment can however be more than just variables. Like parameters of subfunctions, they can also be constants, list matches, pairs, tagged values and can have predicates.
Also instead of assigning into variables you can assign into keys of a dictionary. By using `variable.key` instead of a variable, the `key` gets replaced by the value. You can also use the `key@value` syntax
d = [
"foo" -> 1
]
d.bar = 2
# d is now the dictionary ["foo" -> 1, "bar" -> 2]
key = "baz"
[d@key _] = [3 4]
# d is now the dictionary ["foo" -> 1, "bar" -> 2, "baz" -> 3]
The leftmost part must be a variable that contains a dictionary. Keys into that dictionary must either point to a dictionary or must not yet exist (in which case an empty dictionary is created automatically).
d = [->]
d.foo = 1
# d.foo.bar = 2 # Would result in an error as d.foo is not a dictionary.