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