a pretty fun language
Find a file
2023-03-30 20:51:30 +02:00
contrib/SublimeTextPackage Add syntax files for sublime text 2023-03-23 23:44:54 +01:00
examples Add quine 2023-03-03 22:25:38 +01:00
src Add / rename functions for loading other sources 2023-03-30 20:51:30 +02:00
webpage Define globals in apfl itself 2023-03-07 21:40:07 +01:00
.gitignore Switch to CMake 2022-09-16 23:04:20 +02:00
CMakeLists.txt Define globals in apfl itself 2023-03-07 21:40:07 +01:00
logo.svg Add a logo :) 2022-01-02 16:45:21 +01:00
README.md Implement symbol values 2023-03-23 23:44:54 +01:00

apfl

- a pretty fun language

- a pseudo-functional language

An attempt at creating a programming language

!!VERY MUCH NOT READY FOR USE!!

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).

Building and running

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.

mkdir -p build
cd build
cmake ..
make
./src/apfl [tokenizer|parser|eval]

Short description

Values

apfl has these types of values:

  • nil - Marks the absence of a value. Also called null or none in other languages.
  • bool - Good ol' true and false
  • numbers - Currently these are double-precision floats. This might be expanded to other numeric types.
  • strings - Immutable byte string
  • lists - An ordered list of values
  • dictionaries - An unordered mapping from arbitrary keys to values
  • symbols - A unique value
  • pairs - A pair of two values
  • 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).

Strings

A string is surrounded by " quotes and can contain arbitrary bytes, with the exception of " (terminates the string) and \ (is used for escaping).

The following escape sequences are recognized:

  • \\: \
  • \": "
  • \n: New line
  • \r: Carriage return
  • \t: Tabulator
  • \0: Zero byte
  • \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:

other-list = [4 5 6]
[1 2 3 ~other-list 7 8 9] # Evaluates to [1 2 3 4 5 6 7 8 9]

An element of the list can be accessed by using the list@index syntax, where the index can be any numerical expression. Indexes start at 0.

list = ['foo 'bar 'baz]
print list@1 # Prints bar
i = 2
print list@i # Prints baz

Dictionary

A dictionary (or dict for short) is created by placing key -> value pairs inside [] brackets. An empty dict can be created with [->].

[->] # An empty dict
["foo" -> 1, "bar" -> 2] # A dictionary that maps the string "foo" to 1 and "bar" to 2.

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")

d = [
    "foo" -> 1
    "bar" -> 2
    "baz" -> 3
]
print d@"foo" # Prints 1
print d.bar   # Prints 2
k = "baz"
print d@k     # Prints 3

Symbols

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

Pairs

A pair is created with the :: operator between two values. Both the left and the right side can be arbitrary values, including other pairs.

The pair operator is right-associative, so this:

pair = a :: b :: c

results in the same pair as this:

tmp := b :: c
pair := a :: tmp

Functions

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 ~:

more-args = [2 3]
even-more-args = [5 6]
foo 1 ~more-args 4 ~even-more-args   # Equivalent to: foo 1 2 3 4 5 6

This also means that instead of using \ to break up a long function call into multiple lines, you can also do this:

some-function ~[
    argument-1
    argument-2
    a-very-long-argument-name
    ~more-arguments
    (a nested function call)
]

# Equivalent to
some-function \
    argument-1 \
    argument-2 \
    a-very-long-argument-name \
    ~more-arguments \
    (a nested function call)

# Equivalent to
some-function argument-1 argument-2 a-very-long-argument-name ~more-arguments (a nested function call)

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.

add10 = { n ->
    + n 10
}

print (add10 1)  # Prints 11
print (add10 32) # Prints 42

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:

hello := {
->
    hello "World"
name ->
    print (concat "Hello, " name)
}

(hello)           # No arguments, the first subfunction matches; prints "Hello, World"
hello "Universe"  # One argument, the second subfunction matches; prints "Hello, Universe"
hello "Foo" "Bar" # Two arguments, no subfunction matches; raises an error

A special variable name is _, it always matches but does not save the argument and can be used as a placeholder, if you don't care about the value.

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:

factorial := {
0 ->
    1
n ->
    * n (fac (- n 1))
}

factorial 5   # prints 120 (1 * 2 * 3 * 4 * 5)

Also a parameter can be a deconstructed pair:

my-pair := 1 :: 2

add-pair := {
a :: b ->
    + a b
}

add-pair my-pair  # prints 3

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 := symbol 'a
b := symbol 'b

foo := {
a:x -> + 10 x
b:x -> * 10 x
}

foo a :: 1   # prints 11, first subfunction was chosen
foo b :: 2   # prints 20, second subfunction was chosen
foo {} :: 3  # Will fail, no subfunction matches

In these examples, we say that the parameter x is tagged with the symbol a / b.

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 := {
->  # No arguments, nothing to print
x ~more-args ->
    print x
    print-many ~more-args
}

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.

is-even := { n ->
    == 0 (mod n 2)  # Checks, if n modulo 2 equals 0
}

foo := {
n?is-even ->
    print (concat "Called with even number " n)
n ->
    print (concat "Called with odd number " n)
}

foo 1  # Prints: Called with odd number 1
foo 2  # Prints: Called with even number 2
foo 3  # Prints: Called with odd number 3

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:

map := {
_ [] -> # Handle empty list
    []
f [x ~more] ->
    [(f x) ~(map f more)]
}

double := { n -> * n 2 }

map double [1 2 3] # Evaluates to [2 4 6]

It's important to remember that subfunctions are checked against the input arguments from top to bottom. The first successful match will be used.

foo := {
-> "subfunction 1"
a b -> "subfunction 2"
1 2 -> "subfunction 3"
}

foo 1 2 # Returns "subfunction 2" even though the 3rd subfunction matches more precisely

Assignments

We've already used simple assignments in the examples above without properly introducing them.

A simple assignment, like the ones we've used already assigns a value from the right hand side of an = equal sign to the variable on the left hand.

foo = 123
print foo # Prints 123

We've also already seen that you can also use := instead of =, if you want to make sure the variable is a variable local to the current function.

foo = 1
({
    foo = 2
})

# Foo is now 2

({
    foo := 3
})

# foo is still 2, as the variable `foo` inside the last immediately called function was a local one

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.

foo = [1 2 3 4]
[first ~middle _] = foo
# first = 1
# middle = [2 3]

pair := 1 :: 2
a :: b = pair
# a = 1
# b = 2

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.
d.bar.baz = 3    # Valid, the missing dict d.bar is automatically created