lantern

wanderland-programming-guide

Wanderland Programming Guide

How to program in Wanderland - where everything is markdown, peek/poke, and arrows.

Core Principles

1. If It's Hard, You're Doing It Wrong

The entire system is designed around simple operations:

  • Peek (read from an address)
  • Poke (write to an address)
  • Goto (change position)

If you're writing complex code, stop. You're fighting the system.

2. Nodes Are Memory Addresses

fortune           # A node (memory address)
fortune.source    # A field in that node (nested address)
fortune.fetch     # A fence in that node (executable code at address)

Dot notation is just address paths. Like assembly language.

3. Fences Are Executable Code

A fence is code that lives at an address. When you peek it, the code executes:

# fortune.fetch is a fence (code at address fortune.fetch)
# Peeking it runs the code and returns the result
result = peek("fortune.fetch")

Markdown is the assembler.

4. Function Calls = Poke Parameters, Peek Result

There is no special "function call" mechanism. It's just:

# Traditional
result = add(5, 3)

# Wanderland (what it actually does)
poke("add.x", 5)
poke("add.y", 3)
result = peek("add.result")  # Triggers execution, returns value

All function calls are poke-peek sequences.

GraphNode Fences (Calling Other Nodes)

The Pattern

# In sanctuary.md
```graphnode:fortune:fetch
source: stuffy
count: 1

**What this does:**

```python
# 1. Poke parameters to the target node
poke("fortune.source", "stuffy")
poke("fortune.count", 1)

# 2. Peek the target fence (triggers execution)
result = peek("fortune.fetch")

# 3. Return result

The Target Node Reads Parameters

# fortune.fetch fence (Python code)
source = "${fortune.source}"   # Read poked value via variable substitution
count = ${fortune.count}       # Read poked value

# Now use them
for i in range(count):
    fortune = fetch_from(source)

Variable substitution (${...}) is just reading from poked addresses.

Reflexes (Middleware Pipelines)

Arrows compose poke-peek operations:

# Simple arrow (poke-peek)
source -> dest

# Expands to:
poke("dest", peek("source"))
# Middleware pipeline
source →[middleware]→ dest

# Expands to:
poke("middleware.data", peek("source"))
poke("middleware.context", {...})
result = peek("middleware.exec")
poke("dest", result)

Arrows are syntactic sugar for poke-peek chains.

Programming Principles

Named Parameters Are Just Dots

Want to pass nested config? Just use dots in the parameter name:

graphnode:fortune:fetch
config.source: stuffy
config.options.format: markdown
config.options.count: 3

Becomes:

poke("fortune.config.source", "stuffy")
poke("fortune.config.options.format", "markdown")
poke("fortune.config.options.count", 3)
peek("fortune.fetch")

Fences Can Reference Anything in the Graph

# fortune.fetch can read from anywhere
threshold = ${config.thresholds.max_fortunes}  # From config node
source = ${fortune.source}                      # From poked param
api_url = ${services.stuffy.url}               # From services node

The whole graph is your memory space.

Side Effects Are Just More Pokes

# After computing result
fortunes = [...]  # Compute result

# Side effect: store to another location
poke("fortune-cache.latest", fortunes)

Return Values Are Variables

When a fence executes, variables in its scope become child nodes:

# fortune.fetch fence creates variable
fortunes = [{"text": "...", "author": "..."}]

# This becomes accessible at:
# fortune.fetch.fortunes

Peeking fortune.fetch executes code. Peeking fortune.fetch.fortunes reads the result.

Common Patterns

Pattern 1: Simple Function Call

graphnode:add:calculate
x: 5
y: 3

Equivalent to: add(5, 3)

Pattern 2: Middleware Transformation

reflex:
  data →[transform]→[validate]→[store]→ result

Equivalent to: result = store(validate(transform(data)))

Pattern 3: Configuration Injection

graphnode:deploy:production
environment.name: staging
environment.region: us-east-1
environment.replicas: 3

Equivalent to:

deploy(environment={
  "name": "staging",
  "region": "us-east-1",
  "replicas": 3
})

The Assembly Language Metaphor

Wanderland Assembly
poke("addr", value) MOV [addr], value
peek("addr") MOV reg, [addr]
goto("node") JMP label
${addr} [addr] (dereference)
node.field.subfield Memory offset
Fence execution CALL addr
Variable in fence Return value in register

You're writing assembler in markdown.

Anti-Patterns (What NOT To Do)

❌ Special Config Injection

# BAD: Complex config injection mechanism
graph = load_node(slug, mode='substituted')
graph = _execute_python(graph, slug, config_overrides={...})

Why bad: You're fighting the system. Parameters are just pokes!

✅ Just Poke It

# GOOD: Simple poke-peek
for key, value in params.items():
    poke(f"{slug}.{key}", value)
result = peek(f"{slug}.{fence}")

❌ Complex Data Passing

# BAD: Trying to pass data through special mechanisms
result = execute_with_context(node, data={...}, config={...})

✅ Poke and Peek

# GOOD: Just poke what you need
poke("node.data", data)
poke("node.config", config)
result = peek("node.exec")

❌ Building Abstraction Layers

# BAD: Wrapper functions that hide poke-peek
def call_node(node, **kwargs):
    # Complex orchestration
    ...

✅ Use Reflexes (Built-In Abstraction)

# GOOD: Arrows are the abstraction
data →[node]→ result

The Golden Rule

Everything is peek, poke, or goto.

If you're doing something that isn't one of these three operations, you're probably overcomplicating it.

Markdown is the assembler. The graph is memory. Fences are code. Arrows are flow control.

That's it. That's the whole system.

Navigation