6.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.

6.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:

  1. Full restart capability. init() and shutdown() must be idempotent. The host may cold-start the application at any time — after a crash, after a failed reload, or on first launch.

  2. 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.

  3. Runtime exception handling. A crash in update() should not corrupt persistent state. The host clears the store on exception, so the application must handle starting from scratch gracefully.

  4. 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 @live macro and decs_live module automate the common cases, but the developer decides what survives and what starts fresh.

  5. 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.

6.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

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()
    }
    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.

6.1.3. Mode detection

The host inspects the script’s exported functions to choose a mode:

  • Lifecycle mode — the script exports init(). The host calls init(), loops update(), and calls shutdown() on exit. This is the primary live-reload mode.

  • Simple mode — the script only exports main(). The host calls main() directly, behaving identically to daslang.exe.

6.1.4. Lifecycle

Normal startup: init()update() loop → shutdown() on exit.

Reload cycle (triggered by file change or POST /reload):

  1. [before_reload] functions are called (save state).

  2. shutdown() is called.

  3. The host recompiles the script.

  4. A new context is created.

  5. [after_reload] functions are called (restore state).

  6. init() is called in the new context.

Failed reload: The host reverts to the old context, 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 pauses, clears the persistent store (potentially corrupted), and sets the error.

6.1.5. Core API

require live_host

6.1.5.1. Lifecycle and timing

Function

Description

is_live_mode() : bool

True when running under daslang-live.exe.

is_reload() : bool

True during the first frame after a reload.

request_exit()

Signal the host to exit after the current frame.

exit_requested() : bool

True after request_exit() or window close.

request_reload(full : bool)

Request a reload. full=true also clears @live vars.

get_dt() : float

Frame delta time in seconds.

get_uptime() : float

Time since process start. Does not reset on reload.

get_fps() : float

Current frames per second.

is_paused() : bool

True when execution is paused.

set_paused(v : bool)

Pause or unpause execution.

6.1.5.2. Persistent store

Function

Description

live_store_bytes(key, data)

Store a byte array under a string key. Survives reloads.

live_load_bytes(key, data) : bool

Load a byte array. Returns false if the key is not found.

get_last_error() : string

Last compilation error (empty string if none).

6.1.6. Reload annotations

Annotation

Description

[before_reload]

Called before shutdown() during a reload. Save state here.

[after_reload]

Called after recompile, before init(). Restore state here.

[before_update]

Called every frame before update(). Used internally by live_api, live_api_stdio, and other transport agents.

The host discovers annotated functions by name prefix (__before_reload_*, __after_reload_*, __before_update_*). Multiple functions with the same annotation are all called.

6.1.7. State persistence

6.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.

6.1.8. Helper modules

Module

Description

live/glfw_live

GLFW window that persists across reloads + synthetic mouse driver.

live/opengl_live

OpenGL screenshot + APNG video recording commands.

live/decs_live

Auto-serialization of DECS entities across reloads.

live/live_commands

[live_command] annotation for transport-callable functions.

live/live_vars

@live variable macro (auto-persistence).

live/live_watch

File watcher (auto-reload on save).

live/live_watch_boost

File watcher with diagnostic commands (recommended over live_watch).

live/live_api_builtins

Built-in commands (status, last_error, reload, reload_full, pause, unpause, shutdown). Required transitively by live_api_stdio so stdio clients can drive lifecycle by name. The HTTP transport exposes the same operations as REST endpoints (GET /status, POST /reload, …) rather than pulling these built-ins in.

live/live_api

REST API server on port 9090 (requires dasHV).

live/live_api_stdio

JSON-RPC 2.0 over stdin/stdout. No dasHV dependency.

live/audio_live

Audio state persistence across reloads.

6.1.8.1. live/glfw_live

The GLFW window handle is stored in the persistent byte store and survives reloads. Key functions:

Function

Description

live_create_window(title, w, h)

Create or retrieve the persistent GLFW window.

live_destroy_window()

Destroy the window (skipped during reload).

live_begin_frame() : bool

Poll events, return false if window should close.

live_end_frame()

Swap buffers.

live_get_framebuffer_size(w, h)

Query framebuffer dimensions.

6.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

mouse_pos

Teleport synthetic cursor. Args: x, y.

mouse_click

Synthetic button press/release. Args: button (0/1/2), action ("press" | "release").

mouse_scroll

Synthetic scroll. Args: x, y (offsets).

mouse_move_to

Animated linear move to (x, y) over duration_ms (default 250). Per-frame lerp posts one cursor event per frame.

mouse_play

Play a scripted timeline. Args: events array of {t_ms, kind, x, y, button, action} where kind is "move" | "button" | "scroll". Between move events the per-frame tick lerps and posts one cursor event per frame so any reader (ImGui, overlays) sees smooth motion.

mouse_stop

Stop playback and clear the queue.

mouse_status

Playback status: playing, elapsed_ms, cursor_x, cursor_y, queue_idx, queue_total.

get_synth_cursor() : tuple<bool; float; float> returns (active, x, y). 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.

6.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
}

6.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.

6.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. Requires dasHV. Lifecycle operations are surfaced as REST endpoints; user [live_command] functions are reached via POST /command.

  • live/live_api_stdio — JSON-RPC 2.0 over stdin/stdout. No dasHV dependency; suitable for embedding in host processes that drive the script via pipes. Lifecycle and user commands are both invoked by name in the method field.

6.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:

  1. live_api_set_port(p) — programmatic; must be called BEFORE the agent constructs (the server binds at construction time and never rebinds).

  2. Script argv --live-port N (or --live-port=N) anywhere in get_command_line_arguments(), including the post--- slice.

  3. 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.

6.1.9.1.1. Endpoints

Method

Path

Description

GET

/status

JSON: fps, uptime, paused, dt, has_error.

GET

/error

Plain text: last compilation error.

POST

/reload

Incremental reload.

POST

/reload/full

Full recompile (clears @live vars).

POST

/pause

Pause execution. Returns 503 if compile error active.

POST

/unpause

Resume execution.

POST

/shutdown

Graceful shutdown.

POST

/command

Dispatch a single [live_command] via JSON body: {"name":"cmd_name","args":{...}}.

POST

/commands

Dispatch a batch of [live_command] in one round-trip. Body is a JSON array of {name,args} objects; response is a JSON array of per-entry results in input order. Continue-on-error: malformed entries surface {"error":...} in their slot.

ANY

*

JSON help with all endpoints and curl examples.

6.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.

6.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

status

JSON: fps, uptime, paused, dt, has_error.

last_error

Last compilation error string (or JSON null if none).

reload

Incremental reload.

reload_full

Full recompile (clears @live vars).

pause

Pause execution.

unpause

Resume execution.

shutdown

Graceful shutdown.

Any user-defined [live_command] is also callable by name.

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.

6.1.10. CLI reference

daslang-live.exe [options] script.das [-- script-args...]

Flag

Description

-project <file>

Project file (.das_project) for module resolution.

-project_root <path>

Project root — the parent of modules/ for daspkg-style module resolution. Equivalent to passing project_root to MCP tools.

-load_module <path>

Directly load a single dynamic-module folder (the one containing .das_module); repeatable. Bypasses the <project_root>/modules/<name> scan and shadows same-basename entries in dasroot and project_root. Use when working on an external module locally without setting up the junction trick.

-dasroot <path>

Override DAS_ROOT.

-cwd

Change to the script’s directory before loading.

-v1syntax

Use v1 syntax (default: v2).

-track-allocations

Track where heap allocations came from.

-heap-report

Dump heap contents on shutdown.

--no-dyn-modules

Skip loading dynamic modules.

--dump-leaks / --no-dump-leaks

Toggle JobStatus / HandleRegistry leak dumps at exit (default: dump).

--live-port <N>

REST API port. Default 9090; range [1, 65535]. The single-instance lock is keyed on this value, so two binaries on different ports coexist on the same host.

--

Separator: everything after is passed to the script. Note that --live-port is recognized BOTH before and after -- so the C++ lock and the .das HTTP server agree on the same source.

-h, --help

Print help.

6.1.11. Examples

Path

Description

examples/daslive/hello/

Minimal GLFW window with background color tuning.

examples/daslive/hello_stdio/

Minimal stdio (JSON-RPC) variant of the hello example. Same set_color command, no dasHV dependency.

examples/daslive/triangle/

DECS + OpenGL shaders, rotating triangle.

examples/games/arcanoid/

Full breakout game: DECS, audio, particles, 30+ live commands.

examples/games/sequence/

Card board game: multi-module, bot AI, tournament runner.

examples/daslive/tank_game/

3D tank combat with dynamic lighting.

examples/daslive/live_vars_demo/

@live variable persistence demo.

examples/daslive/reload_test/

Manual state persistence with [before_reload]/[after_reload].

examples/daslive/test_commands/

[live_command] registration and dispatch.

examples/daslive/test_api/

JSON-based live commands via REST.

examples/daslive/test_api_http/

Full HTTP REST API integration test.

examples/daslive/test_decs_reload/

DECS entity persistence verification.

examples/daslive/test_watch/

File watcher integration test.

6.1.12. Tips and gotchas

  • .das_module changes require restarting daslang-live.exe (module paths are registered at startup only).

  • get_uptime() does not reset on reload — use is_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] functions cannot be defined in the same module that registers them — use a separate module.

  • Failed reload pauses the host — check GET /error or get_last_error().

  • A single-instance lock prevents running two daslang-live.exe processes on the same port. The lock key includes the resolved port (daslang-live-single-instance-<port> on Windows / /tmp/daslang-live-<port>.lock on POSIX), so two instances on different ports coexist.

  • Guard decs::restart() with if (!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)