From fa8ac565a6c38e4a392161a5ef67cc6b8f23921f Mon Sep 17 00:00:00 2001 From: Scott Richmond Date: Thu, 14 Dec 2023 18:25:59 -0500 Subject: [PATCH] Finish a first draft of complete language documentation. --- language.md | 102 +++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 90 insertions(+), 12 deletions(-) diff --git a/language.md b/language.md index e796eee..87365fe 100644 --- a/language.md +++ b/language.md @@ -63,6 +63,12 @@ ns foo { quux } ``` + +### Working with collections +Ludus names are bound permanently and immutably. How do you add something to a list or a dict? How do you get things out of them? + +Ludus provides functions that allow working with persistent collections. They're detailed in [the Prelude](/prelude.md). That said, all functions that modify collections take a collection and produce the modified collection _as a return value_, without changing the original collection. E.g., `append ([1, 2, 3], 4)` will produce `[1, 2, 3, 4]`. (For dicts, the equivalent is `assoc`.) + ## Expressions Ludus is an expression-based language: all forms in the language are expressions and return values. That said, not all expressions may be used everywhere. @@ -70,15 +76,15 @@ Ludus is an expression-based language: all forms in the language are expressions Some expressions may only be used in the "top level" of a script. Because they are the toplevel, they are assured to be statically knowable. These include: `ns`, `use`, `import`, and `test`. ### Non-binding expressions -Some forms may take any expression that does _not_ [bind a name](#Words-and-bindings), for example, any entry in a collection, or the right-hand side of a `let` binding. This is because binding a name in some positions is ambiguous, or nonsensical. +Some forms may take any expression that does _not_ [bind a name](#Words-and-bindings), for example, any entry in a collection, or the right-hand side of a `let` binding. This is because binding a name in some positions is ambiguous, or nonsensical, or leads to unwarranted complications. ### Simple expressions -Many complex forms will only accept "simple" expressions. Formally, simple expressions are either literal (atomic, string, or collection literals) or synthetic expressions. They are expressions which do not take sub-expressions. +Many complex forms will only accept "simple" expressions. Formally, simple expressions are either literal (atomic, string, or collection literals) or synthetic expressions. They are expressions which do not take sub-expressions: no `if`, `when`, `match`, etc. (`do` expressions are currently not simple, but that may be revised.) ## Words and bindings -Ludus uses "words" to bind values to names. Words must start with a lower case ASCII letter, and can subsequently include any letter character (modulo backing character encoding), as well as , `_`, `/`, `?`, `!`, and `*`. +Ludus uses _words_ to bind values to names. Words must start with a lower case ASCII letter, and can subsequently include any letter character (modulo backing character encoding), as well as , `_`, `/`, `?`, `!`, and `*`. -Ludus binds values to names immutably and permanently: no name may ever be re-bound to a different value. (Although see [refs](#references-and-state), below. +Ludus binds values to names immutably and permanently: no name in the same scope may ever be re-bound to a different value. (Although see [refs](#references-and-state), below. Attempting to use an unbound name (a word that has not had a value bound to it) will result in a panic. @@ -100,7 +106,7 @@ Ludus makes extensive use of pattern-matching. Patterns do two jobs at once: the The simplest pattern is the placeholder: it matches against anything, and does not bind a name. It is written as a single underscore: `_`, e.g., `let _ = :foo`. #### Ignored names -If you wish to be a bit more explict than using a placeholder, you can use an ignored name, which is a name that starts with an underscore: `_foo`. This is not bound, is not a valid name, and can be used however much you wish, even multiple times in the same pattern. +If you wish to be a bit more explict than using a placeholder, you can use an ignored name, which is a name that starts with an underscore: `_foo`. This is not bound, is not a valid name, and can be used however much you wish, even multiple times in the same pattern. It's a placeholder, plus a reader-facing description. ### Literal patterns Patterns can be literal atomic values or strings: `0`, `false`, `nil`, `"foo"`, etc. That means you can write `let 0 = 0` or `let :foo = :foo`, and everything will be jsut fine. @@ -232,8 +238,6 @@ Functions are called by placing a tuple with arguments immediately after a funct ### Defining functions Functions have three increasingly complex forms to define them. All of them include the concept of a function clause, which is just a match clause whose left hand side must be a _tuple_ pattern. -### Function forms - #### Anonymous lambda An anonymous lambda is written `fn {tuple pattern} -> {expression}`, `fn (x, y) -> if gt? (x, y) then x else add (x, y)`. Lambdas may only have one clause. @@ -271,10 +275,8 @@ In place of nesting function calls inside other function calls, Ludus allows for ``` let silly_result = do 23 - > mult (_, 2) - > add (1, _) - > sub (_, 2) - > div (_, 9) & silly_result is 5 + > mult (_, 2) > add (1, _) + > sub (_, 2) > div (_, 9) & silly_result is 5 ``` ### Called keywords @@ -317,16 +319,92 @@ Note that `repeat` does two interesting things: 2. Unlike everything else in Ludus, it requires a block. You cannot write `repeat 4 forward (100)`. (Watch this space.) ### `loop`/`recur` +`loop` and `recur` are largely identical to recursive functions for repetition, but use a special form to allow an anonymous construction and a few guard rails. + +``` +let xs = [1, 2, 3, 4] +loop (xs, 0) with { + ([x], sum) -> add (x, sum) & matches against the last element of the list + ([x, ...xs], sum) -> recur (xs, add (x, sum)) & recurs with the tail +} &=> 10 +``` + +It is `loop with { }`. (Or, you can have a single function clause instead of a set of clauses.) + +`recur` is the recursive call. It must be in tail position--`recur` must be the root of a synthetic expression, in return position. (At present, this is not checked. It will be statically verified, eventually.) + +`recur` calls return to the nearest `loop`. Nested `loop`s are probably a bad idea and should be avoided when possible. ## Environment and context: the toplevel +The "toplevel" of a script are the expressions that are not embedded in other expressions or forms: not inside a block, not a member of a collection, not on the right hand side of a binding, not inside a function body. The toplevel-only forms: ### `import` `import` allows for the evaluation of other Ludus scripts: `import "path/to/file" as name`. `import` just evaluates that file, and then binds a name to the result of evaluating that script. This, right now, is quite basic: circular imports are currently allowed but will lead to endless recursion; results are not cached, so each `import` in a chain re-evaluates the file; and so on. ### `use` -`use` loads the contents of a namespace into a script's context. To ensure that this is statically checkable, this must be at the toplevel. +`use` loads the contents of a namespace into a script's context. To ensure that this is statically checkable, this must be at the toplevel. ### `ns` +A namespace is an associative data structure that has some restrictions on it beyond dicts, which they resemble. `ns`es must be declared at the toplevel, to ensure static checkability. At current, accessing an absent member on an `ns` leads to a panic; eventually, it will be checked statically at compile-time. `ns`es must be named: + +``` +ns foo { + bar + baz + :quux 42 +} +``` + +Typically, namespaces are the last thing exported in a script; an `import` then imports a namespace. + +### `test` +A `test` expression (currently not working!--it will blow up your script, although it parses properly) is a way of ensuring things behave the way you want them to. Run the script in test mode (how?--that doesn't exist yet), and these are evaluated. If the expression under `test` returns a truthy value, you're all good! If the expression under `test` returns a falsy value or raises a panic, then Ludus will report which test(s) failed. + +``` +test "something goes right" eq? (:foo, :foo) + +test "something goes wrong" { + let foo = :foo + let bar = :bar + eq? (foo, bar) +} &=> test failed: "something goes wrong" on line 3 +``` + +`test`s must be at the toplevel--or embedded within other tests in _their_ highest level. + +Formally: `test `. + +## Changing things: `ref`s +Ludus does not let you re-bind names. It does, however, have a type that allows for changing values over time: `ref` (short for reference--they are references to values). `ref`s are straightforward, but do require a bit more overhead than `let` bindings. The idea is that Ludus makes it obvious where mutable state is in a program, as well as where that mutable state may change. It does so elegantly, but with some guardrails that may take a little getting used to. + +``` +ref foo = 42 & foo is now bound to a _ref that contains 42_ +add (1, foo) & panic! no match: foo is _not_ a number +make! (foo, 23) & foo is now a ref containing 23 +update! (foo, inc) & foo is now a ref containing 24 +value_of (foo) &=> 23; use value_of to get the value contained in a ref +``` + +### Ending with a bang! +Ludus has a strong naming convention that functions that change state or could cause a panic end in an exclamation point (or, in computer nerd parlance, a "bang"). So anything function that mutates the value held in a reference ends with a bang: `make!` and `update!` take bangs; `value_of` does not. + +This convention also includes anything that prints to the console: `print!`, `report!`, `doc!`, `update!`, `make!`, etc. + +### Ending with a whimper? +Relatedly, just about any function that returns a boolean value is a predicate function--and has a name that ends with a question mark: `eq?` tests for equality; `ref?` tells you if something is a ref or not; `lte?` is less-than-or-equal. ## Errors: panic! in the Ludus script +A special function, `panic!`, halts script execution with any arguments output as error messages. Panics also happen in the following cases: +* a `let` binding has no match against the value of its expression +* a `match` or `when` form has no matching clause +* a function is called with arguments that do not match any of its clauses +* something that is not a function or keyword is called as a function +* `div` divides by zero +* certain error handling functions, like `unwrap!` or `assert!`, are invoked on values that cause them to panic +In fact, the only functions in the Prelude which can cause panics are, at current, `div`, `unwrap!`, and `assert!`. And, of course, `panic!` itself. + +### Result tuples +Instead of exceptions or special error values, recoverable errors in Ludus are handled instead by result tuples: `(:ok, value)` and `(:err, msg)`. So, for example, `unwrap!` takes a result tuple and either returns the value in the `:ok` case, or panics in the `:err` case. + +Variants of some functions that may have undesirably inexplicit behaviour are written as `{name}/safe`. So, for example, you can get a variant of `div` that returns a result tuple in `div/safe`, which returns `(:ok, result)` when everything's good; and `(:err, "division by zero")` when the divisor is 0. Or, `get/safe` will give you a result rather than returning `nil`. (Althougn `nil` punning makes this mostly unncessary. Mostly.)