Merge branch 'main' of alea.ludus.dev:twc/ludus

This commit is contained in:
Scott Richmond 2025-07-10 15:41:18 -04:00
commit 654e2eeae7
2 changed files with 31 additions and 27 deletions

View File

@ -1,8 +1,8 @@
![Ludus logo](logo.png)
## Ludus: A friendly, dynamic, functional language
Ludus is a scripting programming language that is designed to be friendly, dynamic, and functional.
Ludus is a scripting programming language that is friendly, dynamic, and functional.
This repo currently contains a work-in-progress implementation of an interpreter for the Ludus programming language, using [Janet](https://janet-lang.org) as a host language.
This repo contains a work-in-progress implementation of an interpreter for the Ludus programming language, using Rust as the host language.
Ludus is part of the [_Thinking with Computers_ project](https://alea.ludus.dev/twc/), run by Scott Richmond at the University of Toronto, with collaborator Matt Nish-Lapidus; Bree Lohman and Mynt Marsellus are the RAs for the project. Ludus is our research language, which aspires to be a free translation of Logo for the 2020s.
Here are our design goals:
@ -24,12 +24,10 @@ Ludus is emphatically functional: it uses functions for just about everything. T
Also, we believe that Ludus's immutable bindings and persistent or immutable data structures and careful approach to manipulating state lead to a lot of good pedagogical results. Learning a programming language involves learning how to model what's going on inside the computer; Ludus, we think, makes that both simpler and easier.
If you're looking for cognate languages, Ludus takes a _lot_ of design inspiration from Clojure and Elixir (which itself took a lot from Clojure). (The current--quick, dirty, and slow--version of Ludus is written in [Janet](https://janet-lang.org).) Clojure and Elixir are great! If you're asking why you should use Ludus instead of them, you're already at the point where you should be using them. Ludus is, maybe, for the people whom you'd like to work with in 5 years at your Pheonix shop (but even then, probably not).
If you're looking for cognate languages, Ludus takes a _lot_ of design inspiration from Clojure and Elixir (which itself took a lot from Clojure). Clojure and Elixir are great! If you're asking why you should use Ludus instead of them, you're already at the point where you should be using them. Ludus is, maybe, for the people whom you'd like to work with in 5 years at your Pheonix shop (but even then, probably not).
### Status
Pre-alpha, still under active development. Lots of things change all the time.
The current version of Ludus is a pure function that runs in JavaScript as a WASM blob. We have plans for more and better things.
Pre-alpha, still under active development. Lots of things change all the time. Definitely some broken bits, especially around processes.
### Use
Current emphasis is on the web version: https://web.ludus.dev.
@ -44,11 +42,7 @@ Current emphasis is on the web version: https://web.ludus.dev.
* Careful, explicit state management using `box`es
* Clean, concise, expressive syntax
* Value-based equality; only functions are reference types
#### Under construction
* Actor-model style concurrency.
* Faster, bytecode-based VM written in a systems language, for better performance.
* Performant persistent, immutable data structures, à la Clojure.
* Persistent, immutable data structures, à la Clojure
### `Hello, world!`
Ludus is a scripting language. At current it does not have a good REPL. Our aim is to get interactive coding absolutely correct, and our efforts in [ludus-web](https://github.com/thinking-with-computers/ludus-web) are currently under way to surface the right interactivity models for Ludus.

View File

@ -28,6 +28,7 @@ And that's awesome.
### Ludus is expression based
Ludus has no statements, only expressions.
Every expression returns a value, including conditional forms like `if`, `when`, and `match`.
That said, not every kind of expression can go anywhere.
In Ludus, different types of expressions are called _forms_, riffing on the grand Lisp tradition.
@ -40,16 +41,16 @@ Ludus has the following types:
* `:bool`: Boolean--`true` or `false`.
* `:number`: IEEE-754 64-bit floating point numbers. Ludus does not have an integer type. That said, Ludus avoids `NaN` as much as possible.
* `:string`: UTF-8 strings.
* `:keyword`: Keywords are self-identical atoms, evaluating only to themselves. The equivalent of a `Symbol` in Javascript (or a keyword in Clojure or Elixir). (The types in this list--and in Ludus--are represented as keywords.)
* `:tuple`: Fixed-length, fully immutable collections of zero or more values. Tuples are comma-or-newline separated values, surrounded by parentheses: `(1, 2, 3)`.
* `:keyword`: Keywords are self-identical atoms, evaluating only to themselves. The equivalent of a `Symbol` in Javascript (or a keyword in Clojure or an atom in Elixir). (The types in this list--and in Ludus--are represented as keywords.)
* `:tuple`: Fixed-length, fully immutable collections of between zero and 7 values (inclusive). Tuples are comma-or-newline separated values, surrounded by parentheses: `(1, 2, 3)`.
* `:list`: Persistent, immutable ordered list of any number of Ludus values. Lists are comma-or-newline separated values, surrounded by square brackets: `[:foo, :bar, :baz]`.
* `:dict`: Persistent, immutable associative collection of keyword keys and any Ludus values. Dicts are comma-or-newline separated keyword-and-value pairs, introduced by `#{` and closed with a curly brace: `#{:a 1, :b 2}`.
* `:dict`: Persistent, immutable associative collection of keyword keys and any Ludus values. Dicts are comma-or-newline separated keyword-and-value pairs, introduced by `#{` and closed with a curly brace: `#{:a 1, :b 2}`. Keys may be keywords or strings.
* `:fn`: Functions!
* `:box`: A holder for any value, which can change over time. A cognate of Clojure's atom. This is the only place in Ludus you will find mutable state.
* `:box`: A holder for any value, which can change over time. A cognate of Clojure's atom. This is the only data type in Ludus that can hold mutable state.
At current, three other types are planned but not implemented: `:set`, `:pkg`, `:process`.
Ludus does not allow creating new types.
Ludus does not allow creating new nominal types.
### Ludus has a weird comment character
It uses the ampersand--`&`--to introduce comments.
@ -124,8 +125,7 @@ panic! "oh shit"
```
`panic!` may only take a single value, but that value can be a collection.
**Eventually** (not long from now!), Ludus will have actor-style concurrency, and a panic will only bring down a process.
But this is not yet implemented.
Panics bring down processes, and any linked processes. More on processes below.
### Almost everything is a function
Ludus does not have operators.
@ -143,9 +143,10 @@ Everything you'll want to do with Ludus involves the Prelude in some way.
Note that most Prelude function names can, in fact, be shadowed by local bindings in a script.
That said, there are several functions that, for optimization reasons, are "builtin," whose names may never be used, e.g., `add`, `sub`, `eq?`, `inc`, `dec`, and so on.
#### Boolean functions are "special forms"
#### Boolean "functions" are actually "special forms"
`and` and `or` are special, in that they are compiled differently than other functions.
Their arguments are evaluated lazily, rather than eagerly, so they can short-circuit (and prevent panics).
Because of that, they may not be referred to or passed around like other functions.
### Ludus lists and dicts are persistent
Dicts and lists are persistent.
@ -176,7 +177,7 @@ But with two caveats.
(Before the caveats: you can put newlines before `then` and `else`.)
#### Falsy falues: `nil` and `false`
#### Falsy values: `nil` and `false`
The condition (`foo` in the example above) is evaluated not strictly as `true` or `false`.
Ludus "falsy" values are `nil` and `false`.
Everything else is truthy, including `0` and `()` (the empty tuple), and `""` (the empty string).
@ -189,7 +190,7 @@ If you want to throw away a value, you can do that, but you'll need something li
#### The `when` form
If you have multiple conditions you'd like to chain together, `when` forms are what you want.
(Ludus does not have an `else if` form.)
(Ludus does not have an `else if` form, although you can certainly chain `if` forms together.)
`when` puts multiple clauses together, each of which has a left-hand condition expression and a right-hand body expression: `<condition expr> -> <body expr>`
Ludus will evaluate the left-hand expression, and, if it's truthy, evaluate and return the corresponding right-hand expression:
```
@ -255,11 +256,11 @@ if true
:nothing
} &=> :third
```
Blocks can go most anywhere expressions can go.
Blocks can go most anywhere single expressions can go.
### Ludus has synthetic expressions
We have already seen function calls, e.g., `add (1, 2)`.
This is a _synthetic_ expression, which is a chained combination of bound names, tuples, and keywords.
This is a _synthetic_ expression, which is a chained combination of bound names, tuples, keywords, and method calls (more on these below).
The root of a synthetic expression may be either a name or a keyword.
Subsequent terms must either be tuples or keywords.
They are evaluated by applying the second term to the first, then applying the third term to the result of that first application, and applying the fourth to the second result, and so on.
@ -272,6 +273,13 @@ This accesses `:bar` on `foo`, applies the arguments `(1, 2)` to that value (pre
#### Keywords may be called as functions
Following Clojure's example, you may call a keyword as a function: `foo :bar` and `:bar (foo)` are strictly equivalent.
Keywords may also be called when they are not keyword literals but bound names, e.g., this is valid:
```
let foo = :foo
let bar = #{:foo :baz}
foo (bar) &=> :baz
```
### Ludus has function pipelines
In addition to normal function application, Ludus also has function pipelines, equivalent to Elixir's pipelines or Clojure's thread macros.
@ -352,6 +360,7 @@ fn foo? {
}
```
Ludus will print the documentation for a function by means of the `doc!` function.
`doc!` will also display all the patterns in a function's clauses.
### Ludus has a convention of "commands": they end with a bang
By convention, Ludus functions that end in an exclamation point have side effects.
@ -383,15 +392,14 @@ fn fact {
fact (6) &=> 720
```
The difference between these is that Ludus will throw a compile error if `recur` isn't in tail position.
In addition, all clauses in a loop form, and all invocations of `recur` must have the same arity, whereas functions may have clauses of arbitrary arity.
In addition, all clauses in a loop form, and all invocations of `recur` must have the same arity and may not have splats in patterns, whereas functions may have clauses of arbitrary arity.
### Ludus has multiple "levels" of expressions
Not all Ludus expressions can appear anywhere you need an expression.
Ludus has four levels of expressions that restrict where they may go: simple, nonbinding, expressions, and toplevel.
* _Simple_ expressions include all literals as well as bare names and synthetic expressions. They may go anywhere you expect an expression, e.g. in the condition position in if or when forms. But in these positions, you may not use, say, another conditional form, nor bind a name.
* _Nonbinding_ forms include all expressions _except_ those that bind a name. These include all simple expressions, as well as conditional expressions (`if`, `when`, `match`), anonymous lambdas, and `do` pipelines.
* _Simple_ expressions include all literals as well as bare names, synthetic expressions, anonymous `fn` lambdas, `do` forms, and `panic`s. They may go anywhere you expect an expression, e.g. in the condition position in if or when forms. But in these positions, you may not use, say, another conditional form, nor bind a name.
* _Nonbinding_ forms include all expressions _except_ those that bind a name. To simple expressions, nonbinding expressions extend to conditional forms (`if`, `when`, `match`, `receive`).
* _Expressions_ (tout court) include all Ludus expressions, including those that bind names: `let`, named `fn`s, and `box`.
* _Toplevel_ expressions may only go at the root scope of a script. At current, the are not yet implemented (`pkg`, `use`, `test`). These are statically checked.
### Ludus has carefully managed state
At some point, you need state.
@ -450,6 +458,8 @@ fn fact (n) -> {
Let me tell you, this is _wild_ Ludus.
The `loop` there is very weird indeed.
`box`es, also, are much, much slower than variables.
The short version is, if you can possibly avoid it--and you probably can--don't use boxes.
The more complex version is this: