8.19.25. Live reload

Every earlier tutorial’s source banner had two run modes:

  • daslang.exe <script> — standalone; runs main() which loops init / update / shutdown until exit_requested().

  • daslang-live <script> — same script, hosted inside a wrapper process that watches the file, reruns the typer on save, and swaps the new program in without restarting the GLFW window. ImGui context, registered widget state, dock layout, slider values — anything @live or restored via a hook — carry across the reload.

This tutorial walks through the seams the live-reload framework exposes:

  • live_create_window / live_imgui_init — idempotent so they no-op on reload (the preserved ImGui context is reused).

  • live_begin_frame / live_end_frame — per-frame gate / commit. live_begin_frame returns false while the host is paused or in the middle of swapping programs; skip the frame.

  • @live annotation — preserves a global (or struct field) across reload via auto-generated [before_reload] / [after_reload] serializers in the live/live_vars module.

  • [live_command] — registers an HTTP endpoint that the running process exposes; called from curl or any other client. The imgui_force_set / imgui_click / imgui_snapshot surface is built from [live_command] declarations.

  • [before_reload] / [after_reload] — manual save/restore hooks for state @live can’t track (raw pointers, GL textures, C-owned resources).

Source: modules/dasImgui/examples/tutorial/live_reload.das.

8.19.25.1. Walkthrough

The recording can’t trigger a reload, so it exercises the observable surface and asserts each piece: it force-sets the @live VOLUME slider and verifies the value (force_set_verified), clicks the @live PING_BTN twice and verifies click_count (hold_through_voice), then calls the user-defined bump_counter and reset_counter [live_command] endpoints from outside and verifies the counter climbs to 7 and back to 0 (record_check_value on the snapshot readout). A command that stopped reaching the running program would abort the recording.

  1options gen2
  2options _comment_hygiene = true
  3options gc
  4
  5require imgui
  6require imgui_app
  7require opengl/opengl_boost
  8require live/glfw_live
  9require live/live_api
 10require live/live_commands
 11require live/live_vars
 12require live_host
 13require imgui/imgui_live
 14require imgui/imgui_boost_runtime
 15require imgui/imgui_boost_v2
 16require imgui/imgui_widgets_builtin
 17require imgui/imgui_containers_builtin
 18require imgui/imgui_visual_aids
 19require daslib/json public
 20require daslib/json_boost public
 21
 22// =============================================================================
 23// TUTORIAL: live_reload — the daslang-live workflow that every earlier
 24// tutorial implicitly relied on.
 25//
 26// Every tutorial's STANDALONE / LIVE blocks pointed at the same shape:
 27//
 28//   STANDALONE: daslang.exe <script>
 29//   LIVE:       daslang-live <script>
 30//
 31// In the live mode, the script is hosted inside daslang-live: a wrapper
 32// process that runs your `init` / `update` / `shutdown` exports, watches
 33// the source file for edits, and exposes an HTTP server for `imgui_force_set` /
 34// `imgui_click` / `imgui_snapshot` and any user-defined `[live_command]`.
 35// Save the file and daslang-live re-runs the typer/codegen, calls
 36// `[before_reload]` hooks to stash anything that needs surviving, swaps in
 37// the new program, then calls `[after_reload]` to restore. The GLFW
 38// window stays open; the ImGui context, registered widget state, dock
 39// layout, slider values — anything @live or restored via a hook — all
 40// carry across the gap.
 41//
 42// This tutorial demonstrates each piece:
 43//
 44//   1. `live_create_window` / `live_imgui_init` — idempotent, reload-safe
 45//   2. `live_begin_frame()` — per-frame gate; returns false while paused
 46//                              or reloading. Skip the frame if it does.
 47//   3. `@live` on a state struct — value preserved across reload
 48//   4. `[live_command]` — custom HTTP endpoint that the running process
 49//                          exposes; driven from curl just like imgui_force_set
 50//   5. `[before_reload]` / `[after_reload]` — explicit save/restore hooks
 51//                                              for state the framework
 52//                                              doesn't track automatically
 53//
 54// STANDALONE: daslang.exe modules/dasImgui/examples/tutorial/live_reload.das
 55// LIVE:       daslang-live modules/dasImgui/examples/tutorial/live_reload.das
 56//
 57// DRIVE (when running live):
 58//   curl -X POST -d '{"name":"imgui_snapshot"}'                                                       localhost:9090/command
 59//   curl -X POST -d '{"name":"bump_counter"}'                                                          localhost:9090/command
 60//   curl -X POST -d '{"name":"reset_counter"}'                                                         localhost:9090/command
 61//   curl -X POST                                                                                       localhost:9090/reload
 62// =============================================================================
 63
 64// ---- Custom counter that survives reload via @live ----
 65// Plain module-scope var, marked @live so daslang-live's serializer hauls
 66// it across the reload boundary. Non-`@live` globals reinitialise on
 67// reload — see the demo_string field below for that contrast.
 68var private @live g_custom_counter : int = 0
 69// NOT @live — gets reset on each reload (intentional for the demo).
 70var private g_session_string : string = "fresh session"
 71
 72// ---- Custom live_command — externally callable HTTP endpoint ----
 73// imgui_force_set / imgui_click / imgui_snapshot are all `[live_command]`
 74// internally. Users can add their own:
 75struct BumpArgs {
 76    @optional by : int = 1
 77}
 78
 79[live_command(description = "Bump the custom counter by N (default 1).")]
 80def bump_counter(input : JsonValue?) : JsonValue? {
 81    let args = from_JV(input, type<BumpArgs>)
 82    g_custom_counter += args.by
 83    return JV((ok = true, counter = g_custom_counter))
 84}
 85
 86[live_command(description = "Reset the custom counter to 0.")]
 87def reset_counter(_input : JsonValue?) : JsonValue? {
 88    g_custom_counter = 0
 89    return JV((ok = true, counter = 0))
 90}
 91
 92// ---- Reload hooks — fire on reload boundary ----
 93
 94// [before_reload] runs in the OLD program before the new one loads — stash
 95// non-`@live` data for [after_reload] to restore. Usually marking a var `@live`
 96// suffices; hooks are the escape hatch for raw pointers, file handles, GL textures.
 97[before_reload]
 98def private on_before_reload() {
 99    print("[live_reload tutorial] before_reload fired. g_custom_counter = {g_custom_counter}\n")
100}
101
102[after_reload]
103def private on_after_reload() {
104    print("[live_reload tutorial] after_reload fired. g_custom_counter = {g_custom_counter} (preserved by @live)\n")
105}
106
107[export]
108def init() {
109    // Both calls are idempotent on reload — cold start creates the window + ImGui
110    // context; reload reuses the preserved context and skips the duplicate
111    // CreateContext (see live_imgui_init in imgui_live.das).
112    live_create_window("dasImgui live_reload tutorial", 1040, 720)
113    live_imgui_init(live_window)
114    DisableIniPersistence()
115    let io & = unsafe(GetIO())
116    GetStyle().FontScaleMain = 1.5
117
118    // Re-running init resets g_session_string each reload. (Initial-value
119    // assignments at module scope run once at program load, but `init` is
120    // called both on cold-start AND on reload.)
121    g_session_string = "fresh session"
122}
123
124[export]
125def update() {
126    // The frame-gate. Returns false while daslang-live is paused, in the
127    // middle of swapping programs, or in any other state where rendering
128    // would crash or produce garbage. Always early-out on false.
129    if (!live_begin_frame()) return
130
131    begin_frame()
132
133    ImGui_ImplGlfw_NewFrame()
134    apply_synth_io_override()
135    NewFrame()
136
137    SetNextWindowPos(ImVec2(30.0f, 30.0f), ImGuiCond.FirstUseEver)
138    SetNextWindowSize(ImVec2(640.0f, 460.0f), ImGuiCond.FirstUseEver)
139    window(LIVE_WIN, (text = "live_reload", closable = false,
140                      flags = ImGuiWindowFlags.None)) {
141
142        // ---- A slider whose value survives reload (@live in SliderStateFloat) ----
143        text("VOLUME.value is @live - survives reload.")
144        slider_float(VOLUME, (text = "Volume"))
145
146        separator(LR_SEP_1)
147
148        // ---- The custom counter, driven from outside via [live_command] ----
149
150        // Mirror the @live counter into text_show so the snapshot can address it;
151        // g_custom_counter stays the plain @live var, this is just the readout.
152        CTR_TEXT.value = "g_custom_counter = {g_custom_counter}"
153        text_show(CTR_TEXT)
154        text("  - @live, preserved across reload")
155        text("  - mutated externally by `bump_counter` / `reset_counter`")
156
157        separator(LR_SEP_2)
158
159        // ---- Contrast: session string resets each reload ----
160        text("g_session_string = \"{g_session_string}\"")
161        text("  - NOT @live; init() rewrites it each reload")
162
163        separator(LR_SEP_3)
164
165        // ---- A button whose click count is preserved (ClickState is @live) ----
166        if (button(PING_BTN, (text = "Ping (click_count survives reload)"))) {}
167        text("PING_BTN.click_count = {PING_BTN.click_count}")
168    }
169
170    end_of_frame()
171    Render()
172    var w, h : int
173    live_get_framebuffer_size(w, h)
174    glViewport(0, 0, w, h)
175    glClearColor(0.10f, 0.10f, 0.12f, 1.0f)
176    glClear(GL_COLOR_BUFFER_BIT)
177    live_imgui_render()
178
179    // Always match live_begin_frame with live_end_frame to keep the
180    // frame pump alive on the daslang-live side.
181    live_end_frame()
182}
183
184[export]
185def shutdown() {
186    // Idempotent on reload - live_imgui_shutdown skips during reload so
187    // the ImGui context is preserved; only the cold process exit runs
188    // the full teardown.
189    live_imgui_shutdown()
190    live_destroy_window()
191}
192
193[export]
194def main() {
195    // Standalone entrypoint. daslang-live drives init/update/shutdown
196    // directly and ignores main(); plain `daslang.exe <file>` runs this
197    // loop.
198    init()
199    while (!exit_requested()) {
200        update()
201        maybe_collect_gc()
202    }
203    shutdown()
204}

8.19.25.1.1. The reload boundary

A daslang-live reload runs in this order:

  1. File watcher notices a source-tree edit (or an HTTP POST /reload request arrives).

  2. The HOST collects every [before_reload] function and runs them in the OLD program. The live/live_vars module auto-generates one of these per @live global; the user can register more.

  3. Typer + codegen run against the new source. If they fail, the reload aborts and the old program keeps running — get_last_error() surfaces the diagnostic in daslang, and the last_error live command surfaces it over HTTP.

  4. The new program is loaded. [after_reload] hooks run, restoring the saved state (@live first, then user hooks).

  5. The next update() call sees live_begin_frame() == true and normal rendering resumes.

The GLFW window and the OS-level ImGui context survive the swap — only the daslang program is replaced.

8.19.25.1.2. @live preservation

The simplest way to keep a value across reload is the @live annotation on the global (or on individual struct fields). The live/live_vars module synthesizes the matching save/restore hooks at compile time:

var private @live g_custom_counter : int = 0

The serializer uses daslib/archive; primitives, arrays, tables, strings, and any struct whose fields are themselves @live-friendly all work out of the box. Each @live target gets its own storage key, and the saved data carries a hash of the initialization expression — change the initializer in source, and the stale value is discarded automatically. (No “I changed the default to 10 and now my old value of 0 is wrong” foot-gun.)

Boost widget state types (ClickState, SliderStateFloat, ToggleState, WindowState, …) are already structured this way — their value-carrying fields are @live, their pending-flags fields are not. That’s why a slider’s value survives reload but pending_value doesn’t.

8.19.25.1.3. The frame gate

live_begin_frame() is the only way the host signals “do not render this frame”:

def update() {
    if (!live_begin_frame()) return
    // ... NewFrame, your draw calls, Render
    live_end_frame()
}

States that return false:

  • The host is paused (POST /pause from daslang-live or mcp__daslang__live_pause).

  • A reload is in progress (between [before_reload] and [after_reload]).

  • The most recent typer pass failed and the program is “frozen” on the prior version — the next save that compiles will revive it.

Always early-out on false and always pair with live_end_frame() on the success branch.

8.19.25.1.4. [live_command] — user-defined HTTP endpoints

The same [live_command] annotation that registers imgui_force_set / imgui_click / imgui_snapshot works for user functions. The function takes a JsonValue? (the request body’s args field) and returns JsonValue? (echoed back to the caller):

struct BumpArgs {
    @optional by : int = 1
}

[live_command(description = "Bump the custom counter by N (default 1).")]
def bump_counter(input : JsonValue?) : JsonValue? {
    let args = from_JV(input, type<BumpArgs>)
    g_custom_counter += args.by
    return JV((ok = true, counter = g_custom_counter))
}

The endpoint name (bump_counter) is the function name; the HTTP surface routes POST /command requests with {"name":"bump_counter"} to this handler. The handler runs on the GLFW main thread between frames, so it can safely touch daslang globals and ImGui state without locks.

8.19.25.1.5. Manual reload hooks

When @live doesn’t fit — typically because the state is a pointer to a C-owned resource that the new program won’t recognize — declare a pair of hooks explicitly:

[before_reload]
def private on_before_reload() {
    // Stash whatever ``@live`` can't serialize.
    // live_store_bytes / live_store_string store under a string key.
}

[after_reload]
def private on_after_reload() {
    // Re-read the stash and rebuild the in-memory state.
    // live_load_bytes / live_load_string read by the same key.
}

imgui_live.das itself is the canonical example: it serializes the live_imgui_ctx pointer as a uint64 in [before_reload] and re-binds it with SetCurrentContext in [after_reload].

8.19.25.1.6. Init / shutdown idempotence

init runs on both cold-start AND reload. shutdown runs on reload AND process exit. The framework helpers (live_imgui_init / live_imgui_shutdown) are idempotent — they detect the reload case and no-op accordingly — so the script’s init / shutdown exports can be written once without distinguishing cold-start from reload.

User globals initialized at module scope (var x = 0) only run their initializer once at program load — reload starts a NEW program, so that initializer runs again. Use @live (or a [before_reload] hook) for anything you want to preserve. Anything reset INSIDE init rebuilds each reload — the demo’s g_session_string shows that pattern.

8.19.25.1.7. Standalone vs live

Run standalone with daslang.exe — every part of this tutorial still works EXCEPT the [live_command] HTTP endpoints, which need the daslang-live host. The reload hooks are silent in standalone mode (they only fire on the reload boundary, which never happens).

8.19.25.1.8. Driving from outside

Standard live-command shape; the user-defined endpoints sit next to the built-in ones:

# built-in: snapshot every registered widget
curl -X POST -d '{"name":"imgui_snapshot"}' localhost:9090/command

# user-defined: bump the custom counter by 3
curl -X POST -d '{"name":"bump_counter","args":{"by":3}}' localhost:9090/command

# user-defined: reset
curl -X POST -d '{"name":"reset_counter"}' localhost:9090/command

# framework: trigger a reload (file edit also triggers this)
curl -X POST localhost:9090/reload

The imgui_snapshot payload reflects whatever the most recent bump_counter did — daslang globals and live-command results share one in-memory model.

8.19.25.1.9. Next steps

Now that the live-command surface is explicit, next is the driving-from-outside view: the JSON command set the boost layer ships (imgui_force_set / imgui_click / imgui_open / …) treated as its own programming model — a UI that responds to scripted external events the same way it responds to mouse clicks.

See also

Full source: modules/dasImgui/examples/tutorial/live_reload.das

Framework modules: live_host (the host itself — note the bare name, it is the C++ module), live/live_commands (the [live_command] annotation), and live/live_vars (the @live serializer).

ImGui-specific lifecycle: imgui/imgui_live.das — the [before_reload] / [after_reload] pair that preserves the ImGui context pointer is the canonical example of a manual hook.

Previous tutorial: Containers

Boost macros — the macro layer.