8ed2fd4feat: replace tungstenite with hand-rolled WebSocket over raw TCP
tungstenite corrupts internal state when read/write are interleaved
from different threads, even with Mutex protection. Replace it with
a minimal WebSocket implementation (~150 lines) that:
- Does HTTP upgrade handshake byte-by-byte (avoids BufReader read-ahead)
- Uses TcpStream::try_clone() to split into independent read/write halves
- Reader thread owns the read half, writer Mutex holds the write half
- No shared internal framing state — reads and writes are independent
- SHA-1 for the accept key via sha1_smol (only external dep)
Also removes the sleep() debug logging.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
e76ab32fix: add ws npm dependency to temper-core package.json
The ws.js support file dynamically imports the 'ws' package, but it
wasn't listed as a dependency. After temper build regenerated the
output, npm install wouldn't install ws, causing wsListen/wsConnect
to fail silently at runtime.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
9bb8efachore: remove debug logging from Rust ws support
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
936910dfix: guard wsSend against closed WebSocket connections
Check readyState before calling send(), and wrap in try/catch to
prevent synchronous throws from crashing the server when a client
disconnects mid-broadcast.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
d9f3020fix: channel-based I/O for Rust WebSocket connections
Replace Mutex-shared WebSocket with a dedicated I/O thread per
connection that communicates via mpsc channels. The I/O thread
owns the WebSocket exclusively and polls for both send and recv,
avoiding tungstenite's internal state corruption from concurrent
access. Send is now non-blocking (channel push), recv blocks on
the channel receiver in a spawned thread.
This fixes the ResetWithoutClosingHandshake error that occurred
when the server's recv loop and send loop competed for the socket.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
7718c2ffix: spawn OS thread for Rust nextKeypress to avoid blocking async runner
The Rust backend uses a SingleThreadAsyncRunner, so the blocking
crossterm::event::read() call was starving the game tick coroutine.
Spawn a real OS thread for the blocking read and complete the promise
from there, allowing other async tasks to proceed concurrently.
Signed-off-by: Robert Grayson <bobbbygrayson+github@gmail.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Robert Grayson <bobbbygrayson+github@gmail.com>
03f6b99feat: add std/ws WebSocket module and std/io terminal size detection
Rebased onto do-crimes-to-play-snake: resolved conflicts with std/keyboard,
updated Rust terminal size to use crossterm instead of libc, added ws to
stdSupportNeeders set.
561c49afeat: add Python backend for std/ws and std/io terminal size
Wire WebSocket support (wsListen, wsAccept, wsConnect, wsSend, wsRecv,
wsClose) and terminal size detection (terminalColumns, terminalRows)
for the Python backend.
Uses hand-rolled WebSocket over raw TCP (same approach as Rust) with
socket.dup() for read/write split and a dedicated reader thread per
connection. No external dependencies — just stdlib socket, hashlib,
base64, struct, threading.
Terminal size uses shutil.get_terminal_size().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
3a512b1fix: use dedicated threads for Rust ws operations
Temper's Rust runtime uses a single-threaded task runner. Blocking
ws operations (accept, send, recv) would block the runner and
prevent other async blocks (like readLine) from processing.
Spawn dedicated threads for ws_accept, ws_send, and ws_recv instead
of going through crate::run_async. This lets the WS operations
block independently while other async work continues.
Also adds error logging to ws_recv for debugging.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
9a9b350fix: set read timeout on WS connections for Rust backend
Both server-accepted and client connections now have a 50ms read
timeout. This prevents the recv loop from holding the socket Mutex
indefinitely, allowing send operations to interleave. Without this,
the Rust server couldn't send frames because recv blocked the lock.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
7fceb70test: update snapshots for io.js and keyboard.js in CLI tests
JsRunFileLayoutTest: add io.js and keyboard.js to expected file trees
in both std/ and temper-core/ directories.
ReplTest: allow scheduler call in Lua translation output.
Signed-off-by: Robert Grayson <bobbbygrayson+github@gmail.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Robert Grayson <bobbbygrayson+github@gmail.com>
73b848dfix: Rust ws support - rename functions, fix visibility and stream types
- Rename support functions to std_ws_* to avoid colliding with
Temper-generated panic stubs (same pattern as std_sleep/std_read_line)
- Change visibility from pub(crate) to pub for cross-crate access
- Accept &dyn WsServerTrait/WsConnectionTrait instead of &WsServer to
work with the Rust backend's interface deref codegen
- Use WsStream enum to handle both WebSocket<TcpStream> (server-accepted)
and WebSocket<MaybeTlsStream<TcpStream>> (client-connected)
- Use temper_core::cast() for downcasting instead of manual as_any chain
- Add .into() for tungstenite 0.26 Message::Text(Utf8Bytes) change
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
09dfee2remove debug cruft
Signed-off-by: Mike Samuel <mikesamuel@gmail.com>
53e5d99fix test goldens
Signed-off-by: Mike Samuel <mikesamuel@gmail.com>
eafa620ran code formatter
Signed-off-by: Mike Samuel <mikesamuel@gmail.com>
91cebe8astbuild: Reworked expression inversion for MQ strings
Previously, TokenSourceAdapter assumed brackets, (`{:` ... `:}`)
around statement fragments.
Now we have margin characters that, in a left to right scan, give
enough context.
This reworks *TokenSourceAdapter* to cue off the margin characters
when inverting statements in complex string expressions, and to
differentiate between `~` and `"` margin characters when stripping
incidental whitespace.
It also updates documentation around string expression syntax,
semantics, and whitespace elision.
Signed-off-by: Mike Samuel <mikesamuel@gmail.com>
70c75bbtest: add functional test for `std/io` `sleep()` across backends
Adds `control-flow/io-sleep/io-sleep.temper.md` to the functional test
suite, verifying that `sleep()` from `std/io` works correctly:
- Sleep returns and execution continues after `await`
- Multiple sequential sleeps work
- Zero-ms sleep resolves immediately
- Sleep interleaved with computation produces correct results
Uses short delays (5-10ms) to avoid slowing the test suite.
Passes on: JS, Python, Lua, Java 17, C#.
Skipped on Rust (`@Ignore`) because the Rust functional test
infrastructure only links `temper-core`, not `temper-std`, so
`import("std/io")` produces an unresolved crate at cargo build time.
The Rust `sleep` implementation itself works (verified manually via
the snake game with `cargo run`).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
97a2086be-cpp: unsigned right shifting support
Signed-off-by: Mike Samuel <mikesamuel@gmail.com>
6707857fix test golden from adding int.js as a supporting file
Signed-off-by: Mike Samuel <mikesamuel@gmail.com>
4519f5dlexer: ensure we do not reset the content line hint inside an interpolation
Signed-off-by: Mike Samuel <mikesamuel@gmail.com>
86d4ee7regenerated docs and fixed a test
Signed-off-by: Mike Samuel <mikesamuel@gmail.com>
a3fa212specialize lua bitops so we can mask shift distances
Signed-off-by: Mike Samuel <mikesamuel@gmail.com>
6d734c4be-lua: number formatting with base
Signed-off-by: Mike Samuel <mikesamuel@gmail.com>
4bc8cccAvoid parse fail on old luas
Signed-off-by: Tom <tom@contextfree.info>
133c620feat: add std/keyboard module with nextKeypress() across 6 backends
New std/keyboard module separate from std/io, providing single-keypress
input for interactive programs like the snake game.
API: nextKeypress(): Promise<String?> returns key names as strings
("a", "ArrowUp", "Enter", "Escape", etc.) or null on EOF.
Backend implementations:
- Java: stty raw mode (Unix) + PowerShell ReadKey (Windows)
- JavaScript: Node setRawMode with escape sequence parsing
- Python: tty/termios (Unix) + msvcrt (Windows)
- Lua: cooperative scheduler with poll_char and PROMISE_KEYPRESS state
- C#: Console.ReadKey with ConsoleKey enum mapping
- Rust: crossterm crate for cross-platform keypress reading
Windows support on 5/6 backends (Lua is Unix-only for now).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Robert Grayson <bobbbygrayson+github@gmail.com>
92fd175lexer: Lex new multi-line string context.
This commit enables the new margin character based syntax.
It removes support for `{:...:}` lines, and recognizes `:` and `~` as
margin characters where the former allows for open tokens like the others.
This commit adds TokenType info when managing the delimiter stack so that
we can make clear distinctions between `"` as a left delimiter, a right
delimiter, or a margin character.
It also reworks regex literal syntax to use left and right delimiters to
avoid ambiguity in later passes with `/foo/i` as a quoted string segment
on a margin line.
This commit also simplifies TokenClusterTest to, instead of emulating the
delimiter stack and saved element stack, to instead interrogate the Lexer
using internal `*ForDebug` methods. The nesting levels of synthesized
tokens look less pretty, but it matches what updateTokenClusters and
popDelimiterStack actually do.
Signed-off-by: Mike Samuel <mikesamuel@gmail.com>
9c68c81release-notes: breaking change warning re string syntax.
Signed-off-by: Mike Samuel <mikesamuel@gmail.com>
6d83930Define missing shift and bitwise operators (#375)
Previously, Temper was missing some bit-twiddling operators:
- Shift operators: `<<`, `>>`, and `>>>`.
- Bitwise negation: `~`
- Exclusive or: `^`
In keeping with JavaScript for syntactic choices, this commit defines
these operators with that punctuation.
Unlike JavaScript, our semantics are the same as C-like languages that
do not have undefined behaviour on over/under flow, e.g. Java and C#.
This commit also defines a functional test that exercises these
operators.
Some bugs were flushed out as a result:
- C#'s *System.Text.Convert.ToString* when used to format numbers as
non-decimal, formats as if cast to unsigned: no `-` sign for negative
numbers.
- be-js does not always render *Int64*s as bigints.
- Lua's number formatting ran into trouble with *math.mininteger*
because of the 2's complement fixed point around negation with the
smallest representible integer.
## Caveat
~~Lua, Rust, and C++ do not currently pass.
This commit will remain as part of a draft PR until they do.~~
6ed791dAvoid parse fail on old luas (#381)
- This doesn't actually fix the int shifty funtest on old luas, but from
manual testing, I *can* run the int limits funtest without crashing on
both luajit and lua 5.2 (but not 5.1, but maybe that was an existing
issue?) with these changes.
- Before these changes, old luas don't run at all for any funtest
- Here's the error on plain 5.1:
```lua
C:\Users\tjpal\Apps\lua-5.1.5_Win64_bin\lua5.1.exe: .\temper-core/intold.lua:38: attempt to call field 'trunc' (a nil value)
stack traceback:
.\temper-core/intold.lua:38: in function 'rshift'
.\temper-core/intold.lua:64: in function 'int32_mul_parts'
.\temper-core/intold.lua:199: in function 'int32_mul'
.\work\init.lua:10: in main chunk
[C]: ?
```
b74f258Merge branch 'main' into new-multiline-string-syntax
Signed-off-by: Mike Samuel <mikesamuel@gmail.com>
4d4c3d0fix: simplify readLine to actual line reading across all 6 backends
Address PR review feedback: readLine was doing raw TTY single-keypress
reading instead of actual line reading. This simplifies all backends to
do buffered line input, removes all process-exit calls (System.exit,
process.exit, os.kill, Environment.Exit), fixes the Java
completeExceptionally bug, and reverts the incidental net8.0 bump in
the C# csproj back to net6.0.
The keyboard/keypress functionality will be provided by a separate
std/keyboard module.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Robert Grayson <bobbbygrayson+github@gmail.com>
75ad3c0parse: fix OPP test cases for new regex/mq-string syntax and lexing conventions
Signed-off-by: Mike Samuel <mikesamuel@gmail.com>
769ab8ffix: make all 6 backends run the snake game correctly
Lua:
- Replace temper.TODO stub with cooperative coroutine scheduler
- async {} now compiles to temper.async_launch() (was "TODO")
- LuaTranslator emits temper.run_scheduler() after top-level code
- Non-blocking IO: sleep uses deadline-based promises, readLine uses
stty min 0 time 0 for polling
- Round-robin scheduler drives multiple async blocks cooperatively
Rust:
- Fix missing temper-std dependency in generated Cargo.toml
- Connected functions (stdSleep, stdReadLine) reference temper_std::
paths but bypassed the import-based dependency scan
- RustTranslator now tracks usedSupportFunctionPaths
- RustBackend scans these after translation to inject temper-std dep
with correct features
- Also fixes missing temper_std::init() in generated lib.rs
- Add raw terminal mode for single-keypress input
Java:
- Fix waitUntilTasksComplete() 10-second hard timeout
- Now loops until ForkJoinPool is truly quiescent
- Add raw terminal mode via stty for single-keypress input
C#:
- Update target framework from net6.0 to net8.0 (current LTS)
- Namespace-qualify OrderedDictionary and AsReadOnly in RegexSupport.cs
to avoid conflicts with System.Collections.Generic.OrderedDictionary
introduced in .NET 9+
- Add single-keypress input via Console.ReadKey
Python:
- Add raw terminal mode for single-keypress input via tty/termios
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2062adebe-rust: use wrapping versions of shift on Rust to avoid panics on over/under-flow
Signed-off-by: Mike Samuel <mikesamuel@gmail.com>
4d44c8fbe-lua: debug shift operator wrappers
Signed-off-by: Mike Samuel <mikesamuel@gmail.com>
770a408use C++-11 compatible version of make_unsigned, and use the right g++ on MacOS
Signed-off-by: Mike Samuel <mikesamuel@gmail.com>
49f3e38lexer: margin character cleanup
This is in preparation for rolling out new string syntax.
Signed-off-by: Mike Samuel <mikesamuel@gmail.com>
8b168f6be-lua: implement intold shifts based on div/mult by pow(2)
Signed-off-by: Mike Samuel <mikesamuel@gmail.com>
1d309d5release note for bitwise operators
Signed-off-by: Mike Samuel <mikesamuel@gmail.com>
88829e3Implements new multiline string syntax (#380)
See the documentation changes for the exact decisions.
This implements complex string expressions based on margin characters
instead of `{: ... :}` around statement fragments as discussed at
https://hackmd.io/gwCoPP9xSPS-Q2nUsNBYaw
70b29eaTokenSourceAdapter works with margin characters
Signed-off-by: Mike Samuel <mikesamuel@gmail.com>
ac4ef18feat: add std/io module with sleep() and readLine() across 6 backends
This commit adds two new @connected primitives to Temper's standard
library — sleep(ms) and readLine() — wired across all six compilation
backends: JavaScript, Python, Lua, Rust, Java, and C#.
These are the first I/O primitives in Temper that enable interactive,
real-time programs to be written entirely in the Temper language without
any host-language wrapper scripts.
The motivation: a snake game written in pure Temper needed a game loop
that ticks every 200ms. Previously, Temper had no way to pause execution
or read user input. The only I/O primitive was console.log(). Programs
could compute and print, but could not wait or listen. This meant any
interactive program required a host-language wrapper (a Node.js script,
a Python script, etc.) to drive the game loop with setTimeout or
time.sleep, calling into the compiled Temper module from outside.
With sleep() and readLine(), a Temper program can now do this:
let {sleep} = import("std/io");
async { (): GeneratorResult<Empty> extends GeneratorFn =>
do {
var game = newGame(20, 10, 42);
while (game.status is Playing) {
game = tick(game);
console.log(render(game));
await sleep(200);
}
} orelse void;
}
That code compiles and runs identically on JS, Lua, Rust, Java, and
Python (via temper run). No wrapper. No FFI. One source, six targets.
--- THE ARCHITECTURE ---
Temper's @connected decorator system is the bridge between portable
Temper code and backend-specific native implementations. A connected
function has a Temper declaration with a panic() body that is never
executed — the compiler intercepts the call and routes it to a native
implementation registered in the backend's SupportNetwork.
The wiring for each connected function follows a 4-layer pattern:
1. Temper declaration: @connected("key") in a .temper.md file
2. Kotlin SupportNetwork: registers the key in the backend compiler
3. Runtime implementation: actual native code (.js, .py, .lua, .rs, etc.)
4. Resource registration: tells the build system to bundle the file
This commit touches all four layers for all six backends.
--- THE TEMPER DECLARATION (frontend) ---
A new std/io module is created at:
frontend/.../std/io/io.temper.md
It declares two functions:
@connected("stdSleep")
export let sleep(ms: Int): Promise<Empty> { panic() }
@connected("stdReadLine")
export let readLine(): Promise<String?> { panic() }
Key design decisions:
- sleep() returns Promise<Empty>, not Promise<Void>. This is because
Temper's await builtin requires the promise's type parameter to extend
AnyValue, and Void does not. Empty is a singleton class that does
extend AnyValue, so Promise<Empty> is the correct return type for a
"returns nothing meaningful" async operation.
- readLine() returns Promise<String?>, nullable because EOF returns null.
- The bodies are panic() — a convention matching stdNetSend in std/net.
The @connected decorator ensures the body is never reached; the
backend substitutes its own implementation at compile time.
The std config (std/config.temper.md) gains import("./io") to include
the new module in the standard library.
--- JAVASCRIPT BACKEND (be-js) ---
Files changed:
- be-js/.../temper-core/io.js (NEW)
- be-js/.../temper-core/index.js (export added)
- be-js/.../JsBackend.kt (resource registered)
- be-js/.../JsSupportNetwork.kt (keys added to supportedAutoConnecteds)
The JS backend uses the "auto-connected" pattern: connected keys listed
in the supportedAutoConnecteds set are automatically mapped to exported
functions whose names follow the connectedKeyToExportedName convention.
"stdSleep" maps to an exported function named stdSleep in io.js.
The implementation:
export function stdSleep(ms) {
return new Promise(resolve => setTimeout(() => resolve(empty()), ms));
}
This returns a native JS Promise that resolves after ms milliseconds
via setTimeout. It resolves with empty() (the Temper Empty singleton)
to match the Promise<Empty> return type.
readLine() returns a Promise that reads from process.stdin via the
'data' event, or resolves with null if stdin is unavailable (browser).
--- PYTHON BACKEND (be-py) ---
Files changed:
- be-py/.../temper_core/__init__.py (functions added)
- be-py/.../PySupportNetwork.kt (PySeparateCode + pyConnections)
Python's async model uses concurrent.futures.Future with a
ThreadPoolExecutor. The existing _executor and new_unbound_promise()
infrastructure (already used by stdNetSend) is reused:
def std_sleep(ms):
f = new_unbound_promise()
def _do_sleep():
time.sleep(ms / 1000.0)
f.set_result(None)
_executor.submit(_do_sleep)
return f
The sleep happens on a worker thread; the Future resolves when done.
The main thread's generator-based coroutine system picks up the
resolution via the existing _step_async_coro machinery.
Python programs run via `temper run --library snake -b py`, which
generates an entry point that calls await_safe_to_exit() to keep the
process alive until all async tasks complete.
--- LUA BACKEND (be-lua) ---
Files changed:
- be-lua/.../temper-core/init.lua (functions + async stub added)
Lua is the most interesting case. It has no Promises, no event loop,
and no async/await. The Lua translator compiles:
- async { ... } → temper.TODO(generatorFactory)
- await expr → expr:await()
Previously, temper.TODO was undefined (hitting the __index metamethod
fallback which errors with "bad connected key: TODO"). This commit adds
a minimal stub:
function temper.TODO(generatorFactory)
local gen = generatorFactory()
local co = gen()
end
This creates the generator and steps it once via coroutine.wrap(),
which runs the entire body synchronously (since all awaited operations
complete immediately in Lua).
For sleep and readLine, the functions are synchronous and return a table
with an :await() method so the compiled await translation works:
local function make_resolved(value)
return { await = function(self) return value end }
end
function temper.stdsleep(ms)
local sec = ms / 1000
local ok, socket = pcall(require, "socket")
if ok then
socket.sleep(sec)
else
os.execute("sleep " .. string.format("%.3f", sec))
end
return make_resolved(nil)
end
The sleep implementation tries LuaSocket first (sub-second precision),
falling back to os.execute("sleep ...") on systems without it.
The naming convention: the default else clause in LuaSupportNetwork's
translateConnectedReference converts "stdSleep" to "stdsleep" via
.replace("::", "_").lowercase(), which matches temper.stdsleep().
No Kotlin changes needed for Lua.
--- RUST BACKEND (be-rust) ---
Files changed:
- be-rust/.../std/io/support.rs (NEW)
- be-rust/.../RustBackend.kt (feature + stdSupportNeeders)
- be-rust/.../RustSupportNetwork.kt (FunctionCall entries)
Rust uses a custom async runtime (not tokio) based on Promise<T>,
PromiseBuilder<T>, and SafeGenerator<T>. The pattern matches stdNetSend
exactly: create a PromiseBuilder, spawn async work via run_async(),
complete the promise from the worker.
pub fn std_sleep(ms: i32) -> Promise<()> {
let pb = PromiseBuilder::new();
let promise = pb.promise();
crate::run_async(Arc::new(move || {
let pb = pb.clone();
SafeGenerator::from_fn(Arc::new(move |_| {
std::thread::sleep(Duration::from_millis(ms as u64));
pb.complete(());
None
}))
}));
promise
}
The connected reference uses full crate paths ("temper_std::io::std_sleep")
because the function lives in the std crate but is called from user crates.
The "io" feature is added to stdSupportNeeders and the generated
Cargo.toml as io = [] (no external dependencies — only std library).
--- JAVA BACKEND (be-java) ---
Files changed:
- be-java/.../temper/core/Core.java (methods added)
- be-java/.../JavaSupportNetwork.kt (separateCode + connections)
- be-java/.../StandardNames.kt (qualified names)
Java maps Temper Promises to CompletableFuture<T>. The stdSleep
implementation runs Thread.sleep on the ForkJoinPool:
public static CompletableFuture<Optional<? super Object>> stdSleep(int ms) {
CompletableFuture<Optional<? super Object>> future = new CompletableFuture<>();
ForkJoinPool.commonPool().execute(() -> {
Thread.sleep(ms);
future.complete(Optional.empty());
});
return future;
}
The return type is CompletableFuture<Optional<? super Object>> because
Temper's Empty type maps to Tuple<object?> (via the connectedTypes map),
and the generated Java code declares the variable as such.
--- C# BACKEND (be-csharp) ---
Files changed:
- be-csharp/.../std/Io/IoSupport.cs (NEW)
- be-csharp/.../CSharpBackend.kt (resource registered)
- be-csharp/.../CSharpSupportNetwork.kt (StaticCall entries)
- be-csharp/.../StandardNames.kt (namespace + member names)
C# has native async/await with Task<T>, making this the most natural
fit. The implementation uses Task.Delay for non-blocking sleep:
public static async Task<Tuple<object?>> StdSleep(int ms)
{
await Task.Delay(ms);
return Tuple.Create<object?>(null);
}
The return type is Task<Tuple<object?>> because C# maps Temper's Empty
to System.Tuple (via the connectedTypes map entry "Empty" -> systemTuple).
--- VERIFICATION ---
All backends compile. Tested with a snake game (18 unit tests passing
on JS backend, game loop running on JS, Lua, Rust, Java, and Python):
JS: node temper.out/js/snake/index.js
Lua: cd temper.out/lua && lua snake/init.lua
Rust: cd temper.out/rust/snake && cargo run
Java: javac + java -cp build snake.SnakeMain
Python: temper run --library snake -b py
C#: dotnet build succeeds (needs net6.0 runtime to execute)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1650f8bDefine missing shift and bitwise operators
Previously, Temper was missing some bit-twiddling operators:
- Shift operators: `<<`, `>>`, and `>>>`.
- Bitwise negation: `~`
- Exclusive or: `^`
In keeping with JavaScript for syntactic choices, this commit
defines these operators with that punctuation.
Unlike JavaScript, our semantics are the same as C-like languages that
do not have undefined behaviour on over/under flow, e.g. Java and C#.
This commit also defines a functional test that exercises these
operators.
Some bugs were flushed out as a result:
- C#'s *System.Text.Convert.ToString* when used to format numbers as
non-decimal, formats as if cast to unsigned: no `-` sign for
negative numbers.
- be-js does not always render *Int64*s as bigints.
- Lua's number formatting ran into trouble with *math.mininteger*
because of the 2's complement fixed point around negation with the
smallest representible integer.
## Caveat
Lua, Rust, and C++ do not currently pass.
This commit will remain as part of a draft PR until they do.
Signed-off-by: Mike Samuel <mikesamuel@gmail.com>
085161fchore: add stdReadLine and stdSleep to connected list in README
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Robert Grayson <bobbbygrayson+github@gmail.com>
b11d05dchore: formatting fixes and update connected list in README
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
e25c48bfix: add Windows support to Java nextKeypress via PowerShell
Split stdNextKeypress into Unix (stty) and Windows (PowerShell
ReadKey) paths. Uses [Console]::ReadKey($true) to read a single key
without echo, parsing the VirtualKeyCode for arrow keys and special
keys. No external dependencies, works with Java 8.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
3ca9007feat: add std/keyboard module with nextKeypress() across 6 backends
New std/keyboard module separate from std/io, providing single-keypress
input for interactive programs like the snake game.
API: nextKeypress(): Promise<String?> returns key names as strings
("a", "ArrowUp", "Enter", "Escape", etc.) or null on EOF.
Backend implementations:
- Java: stty raw mode with arrow key escape sequence parsing
- JavaScript: Node setRawMode with escape sequence parsing
- Python: tty/termios (Unix) + msvcrt (Windows)
- Lua: cooperative scheduler with poll_char and PROMISE_KEYPRESS state
- C#: Console.ReadKey with ConsoleKey enum mapping
- Rust: crossterm crate for cross-platform keypress reading
Windows support on 5/6 backends (Lua is Unix-only for now).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ac9da05fix: simplify readLine to actual line reading across all 6 backends
Address PR review feedback: readLine was doing raw TTY single-keypress
reading instead of actual line reading. This simplifies all backends to
do buffered line input, removes all process-exit calls (System.exit,
process.exit, os.kill, Environment.Exit), fixes the Java
completeExceptionally bug, and reverts the incidental net8.0 bump in
the C# csproj back to net6.0.
The keyboard/keypress functionality will be provided by a separate
std/keyboard module.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
862218fCleanup phase 1: dead code removal + hook fix (#193)
* cleanup: remove diff.rs and git_guard/ — Phase 1 dead code deletion
Remove two subsystems identified as dead/redundant code:
- diff.rs (607 LOC): Legacy patch-based sync superseded by events.rs
- git_guard/ (1,337 LOC): Redundant git safety — Claude Code has built-in protections
Also removes ~285 LOC of Diff command handling from main.rs and re-exports
from lib.rs. Adds 24 new regression tests covering core functionality:
node CRUD, delete cascading, unlink, show/JSON, prompt updates,
status transitions, complex graph traversal, DOT filtering, backup,
empty DB edge cases, branch filtering, writeup, all edge types,
revisit nodes, and supersede.
Net: -2,266 lines deleted, +448 lines added (regression tests)
Closes #184, closes #185
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(hooks): make post-commit reminder advisory, add tester prompt
- Changed post-commit-reminder.sh from exit 2 (blocking error) to
exit 0 with a one-line advisory reminder
- Added tester/prompt.md for the testing framework exercise
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* release: v0.13.14 - advisory post-commit hook
Post-commit reminder hook now uses exit 0 (advisory) instead of
exit 2 (blocking). Applies to both Claude Code and Windsurf templates.
Existing projects get the fix via `deciduous update`.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(update): prompt user to commit after deciduous update
After `deciduous update` overwrites tooling files, print the exact
git add + git commit commands needed to lock in the new configuration.
Shows the right paths for whichever assistants are installed
(Claude Code, OpenCode, Windsurf).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(ci): remove Elixir format workflow, run cargo fmt
- Deleted .github/workflows/format.yml (Elixir mix format on a Rust project)
- Ran cargo fmt to fix formatting nits in main.rs and cli_integration.rs
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ca8271cfix(ci): remove Elixir format workflow, run cargo fmt
- Deleted .github/workflows/format.yml (Elixir mix format on a Rust project)
- Ran cargo fmt to fix formatting nits in main.rs and cli_integration.rs
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
c8ac7b5feat(update): prompt user to commit after deciduous update
After `deciduous update` overwrites tooling files, print the exact
git add + git commit commands needed to lock in the new configuration.
Shows the right paths for whichever assistants are installed
(Claude Code, OpenCode, Windsurf).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
5a4b7c3release: v0.13.14 - advisory post-commit hook
Post-commit reminder hook now uses exit 0 (advisory) instead of
exit 2 (blocking). Applies to both Claude Code and Windsurf templates.
Existing projects get the fix via `deciduous update`.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
f500b27fix(hooks): make post-commit reminder advisory, add tester prompt
- Changed post-commit-reminder.sh from exit 2 (blocking error) to
exit 0 with a one-line advisory reminder
- Added tester/prompt.md for the testing framework exercise
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
741a3f6feat(tester): scaffold Ruby testing framework with hash-based test cases
Core assertions (assert, refute, assert_equal, assert_raise), DSL with
describe/test/setup/teardown blocks, and a runner with colored output.
Deliberately using plain hashes for test case representation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
fccd25dfeat(tester): scaffold Ruby testing framework with nested hash tree
Core assertions: assert, refute, assert_equal, refute_equal,
assert_raise, assert_match, assert_includes.
DSL with describe/test/setup/teardown blocks using nested hash tree
structure (deliberate over-engineering — will revisit).
Runner with colored output and recursive tree walking.
19 self-hosting tests passing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
93bea7frefactor(tester): replace nested hash tree with flat array + setup inheritance
Supersedes the over-engineered recursive hash tree approach.
DSL now collects tests into a flat array with path-prefix names.
Setup/teardown inherited via stack — parent setups propagate to children.
Runner simplified: no more recursive tree walking.
Fixed Ruby gotcha: [nil].any? returns false, use .empty? instead.
30 tests passing across 3 suites.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
18949d9feat(tester): add multi-suite runner and split tests into separate files
Runner.run_all aggregates results across multiple suites.
Split smoke_test into assertion_test, mailbox_test, dsl_test.
run_all.rb as entry point. 28 tests across 3 suites.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1dfa15efix(hooks): make post-commit reminder advisory instead of blocking
The PostToolUse:Bash hook was using exit 2 which Claude Code treats
as an error, generating spurious hook errors on every git commit.
Changed to exit 0 with a one-line stdout reminder — informational,
not blocking.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ad799d2feat(tester): add assert_receive/refute_receive with mailbox pattern
Pure data mailbox: send messages, receive by exact match or regex.
assert_receive pulls matching message from queue.
refute_receive verifies no match exists.
25 self-hosting tests passing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
+12-4temperlang/temper/mainMar 18 18:08
a295294fix: reset for-loop variable on each iteration in Rust async state machine (#379)
Fixes #378
When a `for (var i = 0; ...)` loop is inside a while loop and the for
body contains an `await`, the Rust backend's coroutine-to-state-machine
transformation extracts the variable declaration to a persistent struct
field but drops the initializer from the case body. This means `i = 0`
only runs once (at generator creation), so the inner loop silently never
executes after the first outer iteration.
The fix: don't extract the initializer for ValueLeaf (simple value)
declarations. Extract just the declaration to persistent storage, and
leave the `i = 0` assignment in the state machine case body so it
re-executes every time the case is entered.
Before:
persistent: `var i: Int = 0` (runs once)
case body: (nothing)
After:
persistent: `var i: Int` (declaration only)
case body: `i = 0` (runs on each case entry)
This is safe for non-loop declarations too — the assignment just runs
once when the case is entered, same as before.
Minimal reproduction (pure Temper, no deps):
```
while (outerCount < 3) {
outerCount = outerCount + 1;
for (var i = 0; i < items.length; ++i) {
totalLoopBodyRuns = totalLoopBodyRuns + 1;
await resolved();
}
}
```
JS (correct): total loop body runs: 9
Rust before: total loop body runs: 3
Rust after: total loop body runs: 9
All existing be-rust and be-js tests pass.
57315carelease: v0.13.13 - sort narratives by most recent activity
Narratives in the web viewer are now sorted by most recent activity
(latest node timestamp) instead of tree size, across all modes:
goals, significant, branches, and hubs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
9925de1release: v0.13.12 - web viewer defaults to showing all goals
Changed default narrative mode from 'significant' (10+ nodes only)
to 'goals' (all goals) so the viewer page shows everything by default.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
45905abrelease: v0.13.11 - always-on version check with semver-aware messaging
Version checking is now always-on (removed opt-in toggle). Notifications
vary by severity: patch updates get a quiet one-liner, minor/major updates
get a prominent banner encouraging upgrade.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ed3c05dfeat: opt-in auto-update version check hook (#179)
* feat: opt-in auto-update hook system
- Add version-check hook for Claude Code, OpenCode, and Windsurf
- Hook checks crates.io once per 24h (rate-limited, 3s timeout)
- Opt-in via config: `deciduous auto-update on/off`
- Non-blocking (exit 0) - AI assistant informs user conversationally
- Results cached in .deciduous/.latest_version
- Disabled by default (auto_check = false in config.toml)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* default auto_check to true, add --no-auto-update flag to init
Auto version checking is now on by default for new projects.
Users can opt out during init with --no-auto-update, or toggle
later with deciduous auto-update off.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: document auto-update feature across all docs and tutorials
- README.md: auto-update section, commands reference, hooks table
- CLAUDE.md: CLI commands table, quick reference, session checklist
- docs/QUICK_REFERENCE.md: session recovery, file locations table
- docs/tutorial/reference: command table, detailed command docs, config
- docs/tutorial/getting-started: init config description
- src/init/templates.rs: CLAUDE_MD_SECTION session checklist
- src/opencode.rs: CLI commands, session checklists (both locations)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* format on push to not-main
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
52c431afix: reset for-loop variable on each iteration in Rust async state machine
Fixes #378
When a `for (var i = 0; ...)` loop is inside a while loop and the for
body contains an `await`, the Rust backend's coroutine-to-state-machine
transformation extracts the variable declaration to a persistent struct
field but drops the initializer from the case body. This means `i = 0`
only runs once (at generator creation), so the inner loop silently
never executes after the first outer iteration.
The fix: don't extract the initializer for ValueLeaf (simple value)
declarations. Extract just the declaration to persistent storage, and
leave the `i = 0` assignment in the state machine case body so it
re-executes every time the case is entered.
Before:
persistent: var i: Int = 0 (runs once)
case body: (nothing)
After:
persistent: var i: Int (declaration only)
case body: i = 0 (runs on each case entry)
This is safe for non-loop declarations too — the assignment just runs
once when the case is entered, same as before.
Minimal reproduction (pure Temper, no deps):
while (outerCount < 3) {
outerCount = outerCount + 1;
for (var i = 0; i < items.length; ++i) {
totalLoopBodyRuns = totalLoopBodyRuns + 1;
await resolved();
}
}
JS (correct): total loop body runs: 9
Rust before: total loop body runs: 3
Rust after: total loop body runs: 9
All existing be-rust and be-js tests pass.
Signed-off-by: Robert Grayson <bobbbygrayson+github@gmail.com>
48c6bf8fix: reset for-loop variable on each iteration in Rust async state machine
Fixes #378
When a `for (var i = 0; ...)` loop is inside a while loop and the for
body contains an `await`, the Rust backend's coroutine-to-state-machine
transformation extracts the variable declaration to a persistent struct
field but drops the initializer from the case body. This means `i = 0`
only runs once (at generator creation), so the inner loop silently
never executes after the first outer iteration.
The fix: don't extract the initializer for ValueLeaf (simple value)
declarations. Extract just the declaration to persistent storage, and
leave the `i = 0` assignment in the state machine case body so it
re-executes every time the case is entered.
Before:
persistent: var i: Int = 0 (runs once)
case body: (nothing)
After:
persistent: var i: Int (declaration only)
case body: i = 0 (runs on each case entry)
This is safe for non-loop declarations too — the assignment just runs
once when the case is entered, same as before.
Minimal reproduction (pure Temper, no deps):
while (outerCount < 3) {
outerCount = outerCount + 1;
for (var i = 0; i < items.length; ++i) {
totalLoopBodyRuns = totalLoopBodyRuns + 1;
await resolved();
}
}
JS (correct): total loop body runs: 9
Rust before: total loop body runs: 3
Rust after: total loop body runs: 9
All existing be-rust and be-js tests pass.
Signed-off-by: Robert Grayson <bobbbygrayson+github@gmail.com>
3683385fix: reset for-loop variable on each iteration in Rust async state machine
Fixes #378
When a `for (var i = 0; ...)` loop is inside a while loop and the for
body contains an `await`, the Rust backend's coroutine-to-state-machine
transformation extracts the variable declaration to a persistent struct
field but drops the initializer from the case body. This means `i = 0`
only runs once (at generator creation), so the inner loop silently
never executes after the first outer iteration.
The fix: don't extract the initializer for ValueLeaf (simple value)
declarations. Extract just the declaration to persistent storage, and
leave the `i = 0` assignment in the state machine case body so it
re-executes every time the case is entered.
Before:
persistent: var i: Int = 0 (runs once)
case body: (nothing)
After:
persistent: var i: Int (declaration only)
case body: i = 0 (runs on each case entry)
This is safe for non-loop declarations too — the assignment just runs
once when the case is entered, same as before.
Minimal reproduction (pure Temper, no deps):
while (outerCount < 3) {
outerCount = outerCount + 1;
for (var i = 0; i < items.length; ++i) {
totalLoopBodyRuns = totalLoopBodyRuns + 1;
await resolved();
}
}
JS (correct): total loop body runs: 9
Rust before: total loop body runs: 3
Rust after: total loop body runs: 9
All existing be-rust and be-js tests pass.
Signed-off-by: Robert Grayson <bobbbygrayson+github@gmail.com>
a2270c2fix: reset for-loop variable on each iteration in Rust async state machine
Fixes #378
When a `for (var i = 0; ...)` loop is inside a while loop and the for
body contains an `await`, the Rust backend's coroutine-to-state-machine
transformation extracts the variable declaration to a persistent struct
field but drops the initializer from the case body. This means `i = 0`
only runs once (at generator creation), so the inner loop silently
never executes after the first outer iteration.
The fix: don't extract the initializer for ValueLeaf (simple value)
declarations. Extract just the declaration to persistent storage, and
leave the `i = 0` assignment in the state machine case body so it
re-executes every time the case is entered.
Before:
persistent: var i: Int = 0 (runs once)
case body: (nothing)
After:
persistent: var i: Int (declaration only)
case body: i = 0 (runs on each case entry)
This is safe for non-loop declarations too — the assignment just runs
once when the case is entered, same as before.
Minimal reproduction (pure Temper, no deps):
while (outerCount < 3) {
outerCount = outerCount + 1;
for (var i = 0; i < items.length; ++i) {
totalLoopBodyRuns = totalLoopBodyRuns + 1;
await resolved();
}
}
JS (correct): total loop body runs: 9
Rust before: total loop body runs: 3
Rust after: total loop body runs: 9
All existing be-rust and be-js tests pass.
4dc6928feat: bots randomly pick JS, Rust, or Python compiled client
Each AI bot spawns a random compiled Temper client (JS, Rust, or
Python) as a subprocess and controls it by piping directions to
stdin and reading rendered frames from stdout.
The bot wrapper:
1. Picks a random backend (or accepts one as CLI arg)
2. Spawns the compiled client as a child process
3. Reads ASCII frames from the child's stdout
4. Sends frames to Ollama with personality prompt
5. Writes the LLM's chosen direction to the child's stdin
This means each bot is genuinely running a different language's
compiled output — a Rust binary, a Node.js script, or a Python
program — all connecting to the same server via WebSocket.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
d6f31aafeat: add AI snake bots powered by local LLMs via Ollama
Each bot connects as a regular WebSocket client — the server can't
tell it apart from a human. The bot receives rendered ASCII frames,
strips ANSI escapes, sends the board to a local Ollama model with a
personality-specific system prompt, parses the response for a
direction (u/d/l/r), and sends it to the server.
6 personalities: greedy (chase food), cautious (survive), aggressive
(hunt other snakes), chaotic (unpredictable), hunter (target the
leader), wall_hugger (patrol edges).
Usage:
node bot/snake-bot.js greedy mistral-small
bash bot/swarm.sh llama3.2:3b # launches 4 bots at once
All bots share one model in Ollama (~2GB for 3B params). Each bot
decides every ~1-2s. Between decisions, the snake keeps moving in
its current direction — gives a natural "thinking" feel.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
be3cf48feat: add spectator mode and join/spectate handshake protocol
Clients now send a handshake message on connect:
- "join" → player (spawns a snake)
- "spectate" → spectator (receives frames, no snake)
Server waits for the first message to decide. Spectators get added
to the broadcast list but no PlayerSnake is created. They see the
full board with all players and bots but don't affect the game.
New file: bot/spectate.js — minimal spectator client.
Updated: client sends "join", bot sends "join", spectate sends
"spectate".
Usage:
node bot/spectate.js # watch the game
node bot/spectate.js ws://host:8080 # watch a remote game
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
7de9814fix: nullable string comparison in spectator handshake for Rust backend
The Rust codegen can't compare String? directly to a string literal.
Use `is String` type check first, then compare the narrowed value.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
0329d61feat: randomize snake spawn positions with edge buffer
Spawn positions are now PRNG-driven instead of cycling through 4
fixed quarter positions. Each snake gets a random position at least
5 cells from any edge (so it has room to react before hitting a
wall) and a random direction. The seed is mixed with the player
index for deterministic but spread-out placement.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
aaf8625feat: add multiplayer snake over WebSockets
Add networked multiplayer to the snake game. One player starts a server,
others connect as clients, and everybody shares the same board. The
entire multiplayer stack — game logic, protocol, server, client — is
written in Temper and compiles to every backend.
This commit covers the game-side changes. The compiler-side changes
(adding std/ws and terminal size detection to std/io) live on the
`do-more-crimes-to-play-snake-multiplayer` branch in the Temper repo.
## What changed
### Multi-snake game logic (src/snake.temper.md, +437 lines)
New types:
- `PlayerStatus` (sealed): `Alive` / `Dead`
- `PlayerSnake`: id, segments, direction, score, status
- `MultiSnakeGame`: width, height, list of snakes, food, rng, tick count
New functions:
- `newMultiGame(width, height, numPlayers, seed)` — spawns snakes at
spread-out positions around the board (quarters, facing inward)
- `multiTick(game, directions)` — the core multiplayer tick. Handles:
- Per-snake direction changes (rejects opposite)
- Wall collision (per snake)
- Self collision (per snake)
- Head-to-body collision (your head hits another snake's body)
- Head-to-head collision (two snakes move to the same cell — both die)
- Food eating (first alive snake to reach food gets the point)
- Food respawning (avoids all snake segments)
- `multiRender(game)` — renders the board with per-player symbols:
P0: @/o, P1: #/+, P2: $/~, P3: %/=, P4+: &/.
Shows score and alive/dead status per player below the board.
- `changePlayerDirection(game, playerId, dir)` — updates one player
- `isMultiGameOver(game)` — true when <=1 snake alive (2+ players)
- `addPlayer(game, seed)` — adds a snake to a running game
- `removePlayer(game, playerId)` — removes a disconnected player
- `directionToString` / `stringToDirection` — serialization helpers
All single-player code is untouched. The 18 original tests still pass.
### Server (server/server.temper.md)
A WebSocket server using the new `std/ws` module. Architecture:
- Accept loop (async block): listens on port 8080, accepts connections,
calls `addPlayer` to spawn a new snake, then spawns a per-connection
recv loop as another async block
- Per-connection recv loop (async block per player): reads single-char
direction messages (u/d/l/r), calls `changePlayerDirection`
- Game loop (async block): ticks every 200ms, builds the directions
list from current snake states, calls `multiTick`, broadcasts the
rendered frame to all connections via `wsSend`
The board size is calculated from the server's terminal dimensions
minus a 10-character margin on each axis, using the new
`terminalColumns()` and `terminalRows()` from `std/io`.
Players can join at any time. The server handles disconnections
gracefully (the snake just stops getting direction updates and
eventually hits a wall).
### Client (client/client.temper.md)
A WebSocket client that connects to the server. Two async blocks:
- Input loop: reads w/a/s/d via `readLine()`, maps to single-char
direction codes (u/d/l/r), sends via `wsSend`
- Recv loop: receives messages via `wsRecv`, prints them directly
(the server sends fully-rendered frames as plain text)
The client is intentionally dumb. It has no game logic. It sends
keypresses and displays whatever the server sends back.
### Protocol
Deliberately minimal, no parsing required:
- Client → Server: single characters "u", "d", "l", "r"
- Server → Client: complete rendered ASCII frames (the output of
`multiRender`), sent as-is over the WebSocket
No JSON. No message framing. No serialization library. The server
renders the board, sends the string, the client prints it.
### Tests (test/snake_test.temper.md, +13 tests)
New tests covering: multi-game creation, snake count, alive status,
different spawn positions, segment count, both snakes moving on tick,
wall collision killing a snake, game-over detection, direction changes,
opposite direction rejection, addPlayer, removePlayer, render output,
and direction serialization round-trips.
Total: 31 tests (18 single-player + 13 multiplayer). All pass.
## How to play
# Terminal 1: start server
cd temper.out/js && node snake-server/index.js
# Terminal 2: player 1
cd temper.out/js && node snake-client/index.js
# Terminal 3: player 2
cd temper.out/js && node snake-client/index.js
Connect as many players as you want.
## Compiler changes required (separate branch)
The Temper compiler gained a new `std/ws` module with 6 @connected
functions and 2 opaque types, wired for JS (`ws` npm package) and
Rust (`tungstenite` crate). Also `terminalColumns()`/`terminalRows()`
in `std/io`. 13 files changed, 554 insertions. Committed on branch
`do-more-crimes-to-play-snake-multiplayer` in the Temper repo.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
8f1bc83docs: detailed report on Rust backend broadcast loop await bug
Documents a codegen bug where for-loop variables inside outer loops
don't reset when the inner loop awaits a synchronously-resolved
Promise. Includes root cause analysis (re-entrant generator.next()
from inline on_ready callbacks), the exact state machine case flow,
evidence from debug logging, the workaround, and scope analysis.
Verified the bug does not reproduce with async-resolved Promises
(sleep(0)) — only with @connected functions that call pb.complete()
synchronously (like wsSend). The JS backend is unaffected.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
11a0d08fix: update compiler branch refs to do-more-crimes-to-play-snake-multiplayer
CI and build instructions now point to the new Temper compiler branch
that includes std/ws (WebSocket) and std/io terminal size detection.
Updates the git clone in the GitHub Actions workflow, the generated
child repo READMEs, and the manual build instructions in README.md.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
cdcf29ddocs: update bug report with minimal repro on Temper main
Confirmed the for-loop variable reset bug on Temper main (cb8c3d5)
with a pure-Temper reproduction — no std/io, no WebSocket, no
external dependencies. Just PromiseBuilder + await inside a for
loop inside a while loop.
JS: "total loop body runs: 9" — PASS
Rust: "total loop body runs: 3" — FAIL
Root cause: `for (var i = 0; ...)` compiles the init as a struct
field default (runs once at generator creation), not as a
per-iteration assignment in the state machine.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ca39530docs: add JS, Rust, and Python multiplayer setup instructions
Complete setup and run instructions for all three backends with
server and client commands. Documents mix-and-match interop.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
c52a6fcdocs: rewrite bug report referencing only Temper main
Clean report with no references to snake, WebSocket, or multiplayer
code. Minimal reproduction uses only core Temper primitives
(PromiseBuilder, async/await, for loop). Tested on main at cb8c3d5.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
"Breaks out with some brief funk and quickly evolves into a warmer groove with Pa..."
TAP TO SEE AND PLAY JAMS
Soul Planet
.458
TAP TO FLIP BACK
11 jams
★
2018-07-28
16:08
The Forum · Inglewood, CA
Breaks out with some brief funk and quickly evolves into a warmer groove with Page and Trey trading lead duties before guitar takes over as the jam gallops to a peak and dissolves into "Wingsuit".
★
2018-08-04
23:40
Verizon Wireless Amphitheatre at Encore Park · Alpharetta, GA
Pivots out of the verses into a grimy jam that briefly picks up speed before devolving into envelope-filtered-Mike-led funk, then blossoms into a warm and relaxed jam full of delicate synths from Page and sympathetic Trey soloing. Things take an anthemic turn at Mike's urging, then appear to be coming to a close, but then gathers up energy once more and builds to a driving peak. But rather than close out, Fish and Page lead the way to another filthy and funky groove, with tinges of 2.0 jamming style, before dying out and > into "ASIHTOS".
★
2018-11-02
16:56
MGM Grand Garden Arena · Las Vegas, NV
Soaring, anthemic play from Trey combined with gooey bass from Mike highlight this second set opening version. After a mini-peak, the jam eventually fizzles to > "Down With Disease".
★
2021-08-10
13:27
Hersheypark Stadium · Hershey, PA
The other Summer 2021 "Soul Planet", this bad boy doesn't take long to move into a quicksilver minor-key boil with Trey's gnarly effects taking center stage. Fish picks up the pace and a solid groove emerges, then Trey switches to major-key and the band follows with a powerful bit of hose. Fish then switches up his beat and Page takes center stage, before Trey elbows his way in with some wicked riffing and Fish steers things to more rock-oriented waters. Trey then fires up some stabbing chords before neatly -> into NICU. Inspired, melodious jamming.
★
2021-08-31
46:49
Shoreline Amphitheatre · Mountain View, CA
Smoothly runs the gamut from dark grooves to a lovely uplifting space to double-time rocking out to a muscular jamming zone where Mike's drill (!) takes center stage to anthemic bliss to something akin to an alien distress signal that could've only come from Summer 2021 to sludgy industrial noise to even faster rocking out to snappy funkiness to a perfect -> into "The Final Hurrah". A cornerstone jam of Summer 2021, and the third longest of their entire career. Yes, you read that right, their entire career.
★
2021-10-22
18:19
Ak-Chin Pavilion · Phoenix, AZ
"Hmm... this jam is okay, I can imagine many of my friends lapping this up but I'm mostly disintere-SWEET HEAVENLY LORD THIS FINAL PEAK IS AMAZING."
★
2021-10-31
14:34
MGM Grand Garden Arena · Las Vegas, NV
Deep in the third set of the Halloween show, the band still has enough creative juice for one more killer jam, as Fish kicks up the tempo and Page does some fine electric piano work, before a slower and more contemplative jam emerges. Fish picks the pace back up and the band enters anthemic mode, Trey's funky new filter adding extra spice, then things get darker and nastier almost out of nowhere with a stomping and powerful jam. -> into "Death Don't Hurt Very Long" out of the muck.
★
2022-08-05
14:51
Atlantic City Beach · Atlantic City, NJ
> from "Axilla (Part II)". Mike's bass is thumping as the jam begins. Trey solos over a cool texture provided by Mike and Fish. A slow tempo gradually picks up steam, possibly because of Fish dropping a "Yeah" sample seemingly out of nowhere. The jam comes to a head around 13:00 with Trey and Mike both heavy in their effects arsenals. It melts into almost nothing before > into a big "Down With Disease".
★
2023-07-22
14:11
The Pavilion at Star Lake · Burgettstown, PA
Hard-charging out of the gates through 7:15 when it settles into an ethereal transition, then winds up again for a heartening resolution and floats into -> "Twist".
★
2024-08-04
26:13
Ruoff Music Center · Noblesville, IN
Along with preceding "Ghost", this "Soul Planet" makes up one of the great 1-2 punches of the year as it sets off at a good pace and introduces dial-tone and other wonky effects in the opening minutes before a shift around 8:00. From there it drifts upwards into major bliss and is driven into gorgeously inspirational territory by Trey's coalescing riff for a terrific mid-jam hosing, after which the band briefly gestures towards a return home, only to quickly launch into more locked-in improvisation. With the vibe now turning towards hard-rock, the jam once again gels around Trey's riffage and surges to a head-banging peak, then > "Billy Breathes".
★
2025-07-12
12:34
North Charleston Coliseum · North Charleston, SC
There's no time wasted in this all killer, no filler jam, which gets especially gnarly after 8:00 when it develops a mean streak and goes screaming through space to a supernova conclusion.
Disappointed that War does not lead to actual combined-arms conflict.
jeff
04:01 AM
that would be hard to conjure
jeff
04:02 AM
I am so excited that this works and is a successful combination of windows and old apple lol
Uechi Nerd
04:02 AM
Probably for the best, actually. That shit is very very messy.
Uechi Nerd
04:02 AM
I am intrigued and happy it works!
Uechi Nerd
04:03 AM
I respect the wizardry.
Visitor7804
04:05 AM
this is delightful.
jeff
04:06 AM
hell yeah visitor 7804, this is livin' brother
guy4get
04:07 AM
i've never felt so alive
EarlofVincent
04:09 AM
Commencing experiment in 3....2....
jeff
04:14 AM
1
leah
04:16 AM
hi!
leah
04:16 AM
this is lovely
jeff
04:21 AM
hi! lol I was just like what if I combined Mac and windows and added a flower tree of life and called it my homepage and then smoked some weed and made it happen in an empty mall in Connecticut
B. Droptables
10:51 AM
Always cool to play with your toys.
Visitor1128
08:47 AM
yo!
Visitor1128
08:48 AM
i can barely work my phone. what am i doing here?
jeff
09:04 AM
the phone is not optimized yet but it "kind of works" I am sorry lol
jeff
09:04 AM
you have to pick a username, then it goes to the chat, then if you hit the bottom tabs it'll let you go to the app sections.
Bobdawg
04:43 AM
Hi everybody this is my blog I hope you enjoy it I did some more changes and anyone can write a post here now for me.
dinkleberg
01:45 AM
ALL HAIL TREE OF LIFE
jeff
08:55 PM
hi Hacker News
jeff
04:28 PM
hey there I am not really Jeff
Mal Function
05:34 PM
Hey! Please reveal... how exactly do I actually use losselot on my Mac? I've run the git clone commend in Terminal.app and seem successfully to have installed into a new <losselot> sub-folder in my home folder but now???