SLIP
There is an old programmer's conceit that, sooner or later, every serious programmer tries to design a language.
SLIP is mine.
I wanted Lisp's structural power: a small language in which code remains available to the program instead of disappearing into compiler machinery. I also wanted the readability of Rebol, whose command-like programs can feel closer to instructions than notation written for a parser.
But Rebol's greedy evaluation can make it difficult to see where one call ends and another begins without already knowing the functions involved. SLIP keeps the readable, left-to-right flow while making evaluation boundaries and program structure more explicit.
I began with the syntax I wanted to write, then built an interpreter in Python. In the spirit of Peter Norvig's small Lisp interpreter, that was the shortest path from an idea about language design to something I could run, break and change. Python is the experimental vehicle, not the identity of the language.
The question behind SLIP is:
How much of a language can emerge from a few visible, composable structures?
The result is a working research prototype, not a syntax proposal. Its code blocks, signatures, scopes, paths, generic functions and resolvers are implemented and tested as parts of one language model.
A Small, Visible Grammar
SLIP uses different forms for different kinds of structure:
| Form | Meaning |
|---|---|
(...) |
evaluate a grouped expression now |
[...] |
preserve code for later execution |
{...} |
describe names, types and bindings with a signature |
#[...] |
ordinary list data |
#{...} |
ordinary dictionary data |
scope #{...} |
a first-class environment, object or prototype |
The distinction is deliberate. Executable structure, binding structure and ordinary data do not all look the same, but none of them is hidden from the program.
Expressions otherwise read in the order they appear:
10 + 5 * 2
-- => 30
10 + (5 * 2)
-- => 20
SLIP does not silently apply an operator-precedence table to rearrange the first expression. The written order is the execution order; parentheses explicitly request a different grouping. Pipes use the same left-to-right model:
10 |add 5 |mul 2
-- => 30
The goal is not to imitate English. It is to make execution recoverable from the page without a large collection of invisible rules.
Control Flow Without Special Syntax
Square brackets contain code without immediately executing it. Curly braces contain a signature: structured information about names to bind, along with optional types and guards.
That makes this possible:
for {i} 0 3 [
print i
]
for is not a special parser form. It is an ordinary SLIP function. {i} arrives as a signature value and [...] arrives as a code value. The function binds the requested name and decides when and where to execute the block.
This is the implementation from SLIP's standard library:
for: fn {vars, start, end, body-block} [
var-name: extract-simple-param vars
for-scope: current-scope
for-scope: for-scope["meta"]["parent"]
step: if [start <= end] [1] [-1]
i: start
while [if [step > 0] [i < end] [i > end]] [
for-scope[var-name]: i
run-with body-block for-scope
i: i + step
]
none
]
This is macro-like extensibility without a separate macro language or a compiler phase that rewrites the source. Functions can receive program structure directly and control its evaluation using the same runtime model as the rest of the language.
A block can also be stored, returned, inspected, edited, combined with other code or executed later. A .slip file read through a path returns code rather than executing it automatically. Code can therefore live behind local or remote resources, be retrieved as data, and run only when the program deliberately chooses to do so.
Scopes Are Values Too
In many languages the execution environment is hidden inside the runtime. In SLIP, a scope is a first-class value.
workspace: scope #{}
result: run-with [
answer: 10 * 2
answer
] workspace
#[result, workspace.answer]
-- => #[20, 20]
Both sides of evaluation are explicit: the code to execute and the scope in which its bindings should live. Scopes can be stored, passed, returned, extended and used as module-like namespaces.
They are also the foundation of SLIP's prototype system.
Scopes And Dispatch Compose Into An Object System
A scope can act as a prototype and inherit from another scope:
Character: scope #{
hp: 100
}
Player: scope #{} |inherit Character
player: create Player
Behaviour does not have to be stored inside the prototype. SLIP functions are generic functions: defining another implementation under the same name adds another dispatch rule.
heal: fn {character: Character, amount} [
character.hp: character.hp + amount
]
heal: fn {player: Player, amount} [
player.hp: player.hp + (amount * 2)
]
player |heal 10
-- => 120
The pipe gives the call a familiar receiver-oriented reading, but heal remains a free generic function. Scopes provide identity, state, prototypes and inheritance; generic functions provide behaviour.
Unlike a conventional class method, dispatch is not restricted to one privileged receiver. Types on several arguments can participate, and a signature can include a value guard:
LockedDoor: scope #{
locked?: true
}
interact: fn {
actor: Player,
target: LockedDoor,
action |where action = `open`
} [
target.locked?: false
]
door: create LockedDoor
player |interact door `open`
door.locked?
-- => false
This turns domain behaviour into explicit rules. A new combination of actors, targets, states or actions can be added as another implementation instead of another branch inside a central conditional tree.
The object system is not a separate tower of classes and method syntax. It emerges from first-class scopes, prototype inheritance, signatures, generic functions and the ordinary call model.
Paths Are The Language's Connective Tissue
SLIP uses one broad path model for names, navigation, queries, updates and resources:
player.hp
players[.hp > 100].name
file://players.json
http://api.example.com/players.json[.hp > 100].name
The important idea is not convenient property access. It is that identity, selection, traversal and external resources should feel related instead of being fragmented across unrelated subsystems.
Paths also connect directly to code-as-data. A block can be stored behind a resource path, loaded without being executed, modified as program structure, and deliberately run later. That makes code addressable in the same general way as other values.
A future slip://module scheme would extend the model to language-level resources: code could refer directly to a module through the same addressing mechanism already used for data and files.
Resolvers Make Ownership Explicit
First-class scopes are useful for objects and environments, but persistent multi-user systems introduce a harder question:
Who owns the truth, and who is allowed to change it?
A resolver is a specialised scope that acts as the authority root for a domain. Transactions dispatch on the resolver, and committed writes must be rooted in the resolver that owns the state.
Combat: resolver #{
hp: #{ "p1": 100 }
}
apply-damage: fn {this: Combat, target-id, amount} [
next: this.hp[target-id] - amount
if [next < 0] [ next: 0 ]
this.hp[target-id]: next
return next
]
Combat |apply-damage "p1" 10
Different resolvers can own different parts of the same world. Cross-domain reads use an explicitly read-only authority path; cross-domain changes must be requested from the owner:
Spatial: resolver #{
room: #{ "p1": "room.square" }
on-fire: #{}
}
apply-ignite: fn {this: Spatial, target-id} [
this.on-fire[target-id]: true
]
apply-fire: fn {this: Combat, spatial: Spatial, target-id, amount} [
room-id: spatial::room[target-id]
spatial |apply-ignite target-id
return room-id
]
Combat may read the room through spatial::room, but it cannot commit a write into Spatial. It must call a transaction owned by Spatial. Ownership is therefore part of the programming model rather than an agreement scattered across application code.
The same pattern applies beyond simulated worlds: workflows, records, ledgers, policy systems and operational applications all contain state with an authoritative owner.
What Exists Today
SLIP is an actively used research prototype. The current implementation includes:
- a Python interpreter and standard library
- left-to-right expressions, pipes and explicit grouping
- first-class code blocks and signature literals
- runtime code execution, inspection, injection and splicing
- first-class scopes, prototypes and inheritance
- generic functions, multidispatch and guarded value dispatch
- paths, collection queries and file/HTTP resource schemes
- modules loaded from files, HTTP resources or code
- resolvers, transactions, references and derived cells
- asynchronous tasks, structured failures, documentation and executable tests
The interpreter remains in Python because fast iteration still matters more than committing to a compiler and toolchain. I am stress-testing the language by using it to build a persistent graphical world, where its rules, paths, prototypes, concurrent actors and long-lived state must work together rather than survive as isolated demonstrations.
Future directions include direct slip:// addressing and references to Koine-based transpilers, allowing small domain-specific languages to be embedded where their notation earns its place without permanently enlarging SLIP's core grammar.