7.1. daslang-live — Live-Reload Application Host
daslang-live is a live-reloading application host for daslang.
Edit a .das file, save it, and the running application picks up
the changes instantly — preserving windows, GPU state, game entities,
and everything stored in the persistent byte store. No restart, no
lost state. Applications range from games to REST APIs to stdio
JSON-RPC integrations and MCP plugins.
7.1.1. Designing for live reload
A live-reload host can recompile your script and swap in the new code, but it cannot know what matters to your application. A GLFW window handle, a network socket, a game score, an audio buffer — these are application-specific. The host provides the machinery; the application must cooperate.
Five requirements every live-reloadable application must handle:
Full restart capability.
init()andshutdown()must be idempotent. The host may cold-start the application at any time — after a crash, after a failed reload, or on first launch.Compilation failure recovery. A typo should not kill a running application. The host reverts to the old code and pauses, but the application must tolerate being frozen mid-frame until the next successful reload.
Runtime exception handling. A crash in
update()should not corrupt persistent state. The host clears the store’s data entries on exception and stops callingupdate(), so the application must handle starting from scratch gracefully.State persistence. GPU resources (windows, buffers, shaders) and game state (entities, scores) live outside the script context. The application must explicitly save and restore what matters across reloads. The
@livemacro anddecs_livemodule automate the common cases, but the developer decides what survives and what starts fresh.Instance management. Only one instance should run. The host enforces this via a system mutex, but the application’s external resources (ports, files) must also be safe for single-instance operation.
This is by design: explicit control over persistence is safer than magical state preservation that silently carries stale data.
7.1.2. Quick start
Run the hello example:
bin/Release/daslang-live.exe examples/daslive/hello/main.das
A GLFW window opens with a colored background. Edit main.das,
save — the window stays open and the new code takes effect.
Here is the full hello/main.das:
options gen2
options persistent_heap
options gc
require live/glfw_live
require opengl/opengl_boost
require live/live_commands
require live/live_api
require daslib/json
require daslib/json_boost
require live_host
// --- State ---
var bg_r = 0.2f
var bg_g = 0.3f
var bg_b = 0.5f
var frame_count : int = 0
// --- Live commands ---
[live_command]
def set_color(input : JsonValue?) : JsonValue? {
if (input != null && input.value is _object) {
let tab & = unsafe(input.value as _object)
var rv = tab?["r"] ?? null
if (rv != null && rv.value is _number) {
bg_r = float(rv.value as _number)
}
var gv = tab?["g"] ?? null
if (gv != null && gv.value is _number) {
bg_g = float(gv.value as _number)
}
var bv = tab?["b"] ?? null
if (bv != null && bv.value is _number) {
bg_b = float(bv.value as _number)
}
}
return JV("\{\"r\": {bg_r}, \"g\": {bg_g}, \"b\": {bg_b}}")
}
// --- Lifecycle ---
[export]
def init() {
live_create_window("Hello daslive", 640, 480)
print("hello: init (is_reload={is_reload()})\n")
if (!is_reload()) {
frame_count = 0
}
}
[export]
def update() {
if (!live_begin_frame()) {
return
}
frame_count++
if (frame_count % 300 == 0) {
print("hello: frame {frame_count}, dt={get_dt()}, uptime={get_uptime()}\n")
}
var w, h : int
live_get_framebuffer_size(w, h)
glViewport(0, 0, w, h)
glClearColor(bg_r, bg_g, bg_b, 1.0)
glClear(GL_COLOR_BUFFER_BIT)
live_end_frame()
}
[export]
def shutdown() {
print("hello: shutdown (frames={frame_count})\n")
live_destroy_window()
}
// Dual-mode: also works with regular daslang.exe
[export]
def main() {
init()
while (!exit_requested()) {
update()
maybe_collect_gc()
}
shutdown()
}
The same script works under both daslang-live.exe (live reload)
and daslang.exe (standalone). Under daslang.exe the main()
function drives the loop; under daslang-live.exe the host calls
init(), update(), and shutdown() directly.
For a stdin/stdout JSON-RPC transport instead of HTTP, swap one require
line and use examples/daslive/hello_stdio/:
require live/live_api_stdio // instead of live/live_api
User commands (any [live_command]) work identically over both
transports. The two transports differ in wire protocol and in how
built-in lifecycle commands are surfaced; see Stdio API (live/live_api_stdio) below.
7.1.3. Mode detection
The host inspects the script’s exported functions to choose a mode:
Lifecycle mode — the script exports
init(). The host callsinit(), loopsupdate(), and callsshutdown()on exit. This is the primary live-reload mode.Simple mode — the script only exports
main(). The host callsmain()directly, behaving identically todaslang.exe.
7.1.4. Lifecycle
Normal startup:
init() → update() loop → shutdown() on exit.
Reload cycle (triggered by file change or POST /reload):
[before_reload]functions are called (save state).shutdown()is called.The host recompiles the script.
A new context is created.
[after_reload]functions are called (restore state).init()is called in the new context.
Failed reload:
The host reverts to the old context, re-runs [after_reload] and
init() on it, pauses execution, and stores the compilation error.
Retrieve it via GET /error, the last_error stdio command, or
get_last_error(). The next successful reload unpauses
automatically.
Runtime exception:
The host stores the exception message, pauses, and clears the store’s
data entries (__live_vars_* and __decs_*; infrastructure keys
such as the GLFW window handle and audio handles are preserved). The
context is then marked dead: update() and the [before_update]
hooks are skipped from that point on even if you unpause — only a
reload or a reset recovers. An exception thrown inside a
[live_command] does the same.
7.1.5. Core API
require live_host
7.1.5.1. Lifecycle and timing
Function |
Description |
|---|---|
|
True when running under |
|
True from the start of a reload cycle (it is set before
|
|
Signal the host to exit after the current frame. |
|
True after |
|
Request a reload. |
|
Frame delta time in seconds. |
|
Time since process start. Does not reset on reload. |
|
Current frames per second. |
|
True when execution is paused. |
|
Pause or unpause execution. |
7.1.5.2. Persistent store
Function |
Description |
|---|---|
|
Store a byte array under a string key. Survives reloads. |
|
Load a byte array. Returns |
|
Same, for a string value. |
|
Load a string. Returns |
|
Last compilation error or runtime exception message (empty string if none). |
|
Re-simulate the already-compiled program into a fresh context
(no recompile); clears |
|
Bumped on every terminal reload / reset outcome, success or
failure. Also reported by |
7.1.6. Reload annotations
Annotation |
Description |
|---|---|
|
Called before |
|
Called after recompile, before |
|
Called every frame before |
The host discovers annotated functions by name prefix
(__before_reload_*, __after_reload_*, __before_update_*).
Multiple functions with the same annotation are all called.
7.1.7. State persistence
7.1.7.1. @live variables (recommended)
require live/live_vars
Tag globals with @live and the macro auto-generates
[before_reload]/[after_reload] handlers that serialize them
via Archive. No manual save/restore needed:
options gen2
require live/live_vars
require live_host
var @live counter = 0
var @live name = "world"
var @live values : array<float>
[export]
def init() {
if (!is_reload()) {
counter = 0
name = "world"
values |> clear()
values |> push(1.0)
values |> push(2.0)
values |> push(3.0)
print("init: first run\n")
} else {
print("init: reload - counter={counter}, name={name}\n")
}
}
A hash of each variable’s init expression is stored alongside the data. If you change the default value in code, old stored data is discarded and the new default takes effect — safe format migration.
Works with POD types, strings, enums, arrays, tables, and structs with serializable fields.
@live also works on struct fields: a non-@live global whose
struct has @live fields is preserved field by field, and the
non-marked fields take their source defaults on reload. @live on a
let is a compile error — a constant has nothing to restore.
Full reload (request_reload(true) or POST /reload/full) and
POST /reset clear all @live entries.
7.1.7.2. Manual serialization
For complex cases (handles, non-serializable types), use
[before_reload]/[after_reload] with live_store_bytes/
live_load_bytes and the Archive module. See
examples/daslive/reload_test/ for the full pattern.
7.1.8. Helper modules
Module |
Description |
|---|---|
|
GLFW window that persists across reloads + synthetic mouse driver. |
|
OpenGL screenshot + APNG video recording commands, plus the
|
|
Auto-serialization of DECS entities across reloads. |
|
|
|
|
|
|
|
File watcher (auto-reload on save). |
|
File watcher with diagnostic commands (recommended over
|
|
Built-in commands ( |
|
REST API server on port 9090 (requires |
|
JSON-RPC 2.0 over stdin/stdout. No |
|
Audio state persistence across reloads. |
The live/ prefix is a registered module alias, not a directory:
live/glfw_live, live/opengl_live and live/audio_live live in
modules/dasGlfw/, modules/dasOpenGL/ and modules/dasAudio/
respectively; the rest are in modules/dasLiveHost/live/.
7.1.8.1. live/glfw_live
The GLFW window handle is stored in the persistent byte store and survives reloads. Key functions:
Function |
Description |
|---|---|
|
Create or retrieve the persistent GLFW window. |
|
Destroy the window (skipped during reload). |
|
Poll events, return |
|
Swap buffers. |
|
Query framebuffer dimensions. |
7.1.8.1.1. Synthetic mouse driver
live/glfw_live also provides a synthetic-input timeline driver. Events
flow through dasGLFW’s chain dispatcher, so any listener installed on
the window (ImGui_ImplGlfw, app callbacks, etc.) receives them
indistinguishably from real OS input. Used by the visual-aids demo to
re-record APNG tours from a JSON timeline.
Command |
Description |
|---|---|
|
Teleport synthetic cursor. Args: |
|
Synthetic button press/release. Args: |
|
Synthetic scroll. Args: |
|
Animated linear move to |
|
Play a scripted timeline. Args: |
|
Stop playback and clear the queue. |
|
Playback status: |
get_synth_cursor() : tuple<bool; float; float> returns
(active, x, y). active goes true on the first synthetic event and
stays true for the rest of the session. Overlays that draw a cursor
sprite or motion trail should consult this — when active the
synthetic driver owns the position, and ImGui_ImplGlfw’s per-frame
poll would otherwise overwrite io.MousePos with the real OS cursor on
focused windows.
7.1.8.1.2. Synthetic keyboard driver
The same module carries a keyboard timeline built the same way:
key_press / key_release (one key by GLFW keycode), key_char
(one codepoint through the char callback), key_type (type a whole
string), key_chord (modifiers held around a key), key_play /
key_stop (scripted timeline), and key_status.
get_synth_keys() : tuple<bool; uint; float> reports whether the
synthetic keyboard is active, and live_apply_control_badge(locked)
draws the “input is scripted” affordance.
7.1.8.2. live/decs_live
Require this module and all DECS entities auto-persist across reloads.
Guard decs::restart() with is_reload() to avoid wiping
restored entities:
if (!is_reload()) {
decs::restart() // only on first run, not after reload
}
7.1.8.3. live/live_commands
Functions annotated [live_command] are callable via any installed
transport — HTTP POST /command or stdio
{"method":"name", ...}. Signature:
def cmd_name(input : JsonValue?) : JsonValue?. Convention: prefix
with cmd_. The set_color command in the hello example above
demonstrates the pattern.
The built-in lifecycle commands (status, last_error,
reload, reload_full, pause, unpause, shutdown)
live in live/live_api_builtins. They are pulled in by
live/live_api_stdio so stdio clients can invoke lifecycle
operations by name. The HTTP transport exposes the same operations
as REST endpoints (GET /status, POST /reload, …) — see below.
7.1.9. Transports
Two transport modules ship in-tree. Pick one — they coexist but the typical script requires only one:
live/live_api— HTTP REST API on a TCP port. RequiresdasHV. Lifecycle operations are surfaced as REST endpoints; user[live_command]functions are reached viaPOST /command.live/live_api_stdio— JSON-RPC 2.0 over stdin/stdout. NodasHVdependency; suitable for embedding in host processes that drive the script via pipes. Lifecycle and user commands are both invoked by name in themethodfield.
7.1.9.1. REST API (live/live_api)
require live/live_api starts an HTTP server on port 9090. The bound
port is resolved at module init by:
live_api_set_port(p)— programmatic; must be called BEFORE the agent constructs (the server binds at construction time and never rebinds).Script argv
--live-port N(or--live-port=N) anywhere inget_command_line_arguments(), including the post---slice.Default
9090.
For daslang-live (the binary), pass --live-port N to the binary
itself — the C++ side scans the same full argv and keys its
single-instance lock on the resolved port, so two daslang-live
instances on different ports coexist on the same host. Invalid values
(non-numeric, out of [1, 65535]) fall through to the default, and
the last --live-port on the command line wins unconditionally —
--live-port 19090 --live-port abc resolves to 9090, not 19090.
7.1.9.1.1. Endpoints
Method |
Path |
Description |
|---|---|---|
GET |
|
JSON: |
GET |
|
|
POST |
|
Incremental reload. |
POST |
|
Full recompile (clears |
POST |
|
Re-simulate the compiled program into a fresh context (no
recompile); clears |
POST |
|
Pause execution. |
POST |
|
Resume execution. |
POST |
|
Graceful shutdown. |
POST |
|
Dispatch a single |
POST |
|
Dispatch a batch of |
GET |
|
JSON help with all endpoints and curl examples. There is no catch-all route — any other path returns 404. |
While a compilation error is active, /pause, /unpause,
/command and /commands return 503 with
{"error":…,"hint":"POST /reload to retry"}. /reload,
/reload/full, /reset, /shutdown, /status and /error
stay reachable.
7.1.9.1.2. curl examples
Check status:
curl http://localhost:9090/status
Trigger a reload:
curl -X POST http://localhost:9090/reload
Call a live command:
curl -X POST http://localhost:9090/command \
-d '{"name":"set_color","args":{"r":1.0,"g":0.0,"b":0.0}}'
Call a batch of live commands:
curl -X POST http://localhost:9090/commands \
-d '[{"name":"set_color","args":{"r":0.5}},{"name":"set_alpha","args":{"a":0.5}}]'
When using the daslang MCP server, prefer live_* MCP tools over
curl (see MCP Server — AI Tool Integration). The live_commands MCP tool maps to the
POST /commands batch endpoint above.
7.1.9.2. Stdio API (live/live_api_stdio)
require live/live_api_stdio installs a debug agent that reads
newline-delimited JSON-RPC 2.0 messages from stdin and writes
responses to stdout. No HTTP server is started; no port is opened;
no dasHV dependency.
The method field is the live command name — any
[live_command] function plus the built-ins listed below.
params is passed verbatim to the command as its input
JsonValue?.
Request / response shape:
→ {"jsonrpc":"2.0","id":1,"method":"status"}
← {"jsonrpc":"2.0","id":1,"result":{"fps":60.0,"uptime":3.2,"paused":false,"dt":0.016,"has_error":false}}
→ {"jsonrpc":"2.0","id":2,"method":"set_color","params":{"r":1.0,"g":0.0,"b":0.0}}
← {"jsonrpc":"2.0","id":2,"result":"{\"r\": 1.0, \"g\": 0.0, \"b\": 0.0}"}
→ {"method":"shutdown"} ← (no response: notification)
The result field embeds whatever JSON value the command returned —
an object when the command returned an object (status), a string
when the command returned a string (set_color in the demo returns a
JV(string) so the result is a quoted JSON string), and so on.
Implementation. Parsing, envelope building, notification semantics,
and §6 batch handling all come from JSON-RPC 2.0 envelope + parser, transport-agnostic. The transport
itself is just the stdin reader thread plus a thin
dispatch_command bridge — see handle_jsonrpc_line in
modules/dasLiveHost/live/live_api_stdio.das. Clients that need the
parser / envelope helpers directly should require daslib/jsonrpc.
Framing guarantee. The transport always writes exactly one
response per line on stdout, with no embedded newlines in the envelope.
write_json pretty-prints by default, so the dispatch result is
post-processed by jsonrpc::compact_json_whitespace before being
embedded in the envelope.
Notifications. Per JSON-RPC 2.0 §4.1, a request that omits the
id field is a notification: the server MUST NOT respond.
live_api_stdio honors this — fire-and-forget commands like
{"method":"shutdown"} produce no output. An explicit "id":null
is not a notification; the server responds with "id":null. Parse
errors and invalid requests still emit -32700 / -32600
responses with "id":null.
§6 batch support. Multiple commands in one wire message — a JSON array of requests at the top level — produce a JSON array of responses (in input order, notification entries suppressed). Empty array and top-level malformed JSON yield single error envelopes per spec.
Permissive by default. The jsonrpc member is optional and any
value is accepted; pass strict=true through daslib/jsonrpc for
full §4 compliance. Other §4 rules (method required and string,
id string/number/null only) are always enforced.
Error envelopes follow JSON-RPC 2.0 codes (-32700 / -32600 /
-32602).
Available methods (built-ins from live/live_api_builtins):
method |
Description |
|---|---|
|
JSON: |
|
Last compilation error or runtime exception string (or JSON
|
|
Incremental reload. |
|
Full recompile (clears |
|
Pause execution. |
|
Resume execution. |
|
Graceful shutdown. |
|
JSON object mapping every registered command name to its description. Works over both transports. |
Any user-defined [live_command] is also callable by name.
An unknown method is not a -32601: the dispatcher returns a
successful envelope whose result is the string
"unknown command: <name>". The -32700 / -32600 / -32602
codes only ever come from the envelope layer.
Warning
stdout is the response channel. Scripts that use this transport
must redirect print() to stderr or a log file — calling
print() from the script’s main loop will interleave application
output with JSON-RPC responses and break clients that parse stdout
line-by-line. daslang-live itself logs lifecycle messages to
stdout; either silence them or have the client tolerate
non-JSON lines.
The example at examples/daslive/hello_stdio/ is the HTTP hello
example with the single require line swapped — see the same
set_color command driven over stdio instead of POST /command.
7.1.10. CLI reference
daslang-live.exe [options] script.das [-- script-args...]
Flag |
Description |
|---|---|
|
Project file ( |
|
Project root — the parent of |
|
Directly load a single dynamic-module folder (the one containing
|
|
Override |
|
Change to the script’s directory before loading. |
|
Use v1 syntax (default: v2). |
|
Track where heap allocations came from. |
|
Dump heap contents on shutdown. |
|
Skip loading dynamic modules. |
|
Toggle JobStatus / HandleRegistry leak dumps at exit (default: dump). |
|
REST API port. Default 9090; range |
|
Separator: everything after is passed to the script. Note that
|
|
Print help. |
7.1.11. Examples
Path |
Description |
|---|---|
|
Minimal GLFW window with background color tuning. |
|
Minimal stdio (JSON-RPC) variant of the hello example.
Same |
|
DECS + OpenGL shaders, rotating triangle. |
|
Full breakout game: DECS, audio, particles, 30+ live commands. |
|
Card board game: multi-module, bot AI, tournament runner. |
|
Arcade shooter: GLFW + live commands + |
|
Maze game on DECS with audio and live commands. |
|
Scrolling arcade game. |
|
3D tank combat with dynamic lighting. |
|
|
|
Manual state persistence with |
|
|
|
JSON-based live commands via REST. |
|
Full HTTP REST API integration test. |
|
DECS entity persistence verification. |
|
File watcher integration test. |
7.1.12. Tips and gotchas
.das_modulechanges require restartingdaslang-live.exe(module paths are registered at startup only).get_uptime()does not reset on reload — useis_reload()to detect reloads.Debug agents persist across reloads; their code is not updated on reload (restart required to pick up agent code changes).
[live_command]cannot be used insidelive/live_commandsitself — the[function_macro]that implements it is not available while its own module compiles. That is whylive_watch_boostis a separate file fromlive_watch. Any other module defines and registers its commands in one place.Failed reload pauses the host — check
GET /errororget_last_error().A single-instance lock prevents running two
daslang-live.exeprocesses on the same port. The lock key includes the resolved port (daslang-live-single-instance-<port>on Windows //tmp/daslang-live-<port>.lockon POSIX), so two instances on different ports coexist.Guard
decs::restart()withif (!is_reload())to avoid wiping restored entities.
See also
examples/daslive/ – live-reload example applications
Running It Live – blog post on live-coding philosophy
MCP Server — AI Tool Integration – MCP server with live-reload control tools (live_* tools)