lumen_script/lib.rs
1//! Scripting backend trait (v2) + the `ScriptCommand` enum scripts emit.
2//!
3//! `lumen-core` does not depend on this crate. Scripts run inside
4//! host-generic systems provided by `lumen-script`; those systems
5//! drain the [`ScriptCommand`]s the host produced this tick and forward
6//! them onto the ECS message bus for the embedder's applier.
7//!
8//! Concrete implementations live in runtime modules (`lumen-candela` and the
9//! other hosts under `std/`). Each provides a [`ScriptHost`] that runs
10//! whatever language it speaks, exposing the host-neutral registries (command
11//! sink, signal mirror, per-id handlers, derivations) the generic runtime
12//! drives, and registers its language through [`language::ScriptLanguage`].
13
14#![forbid(unsafe_code)]
15#![warn(missing_docs)]
16
17/// Binding a browser add-on's described functions into the hosts, on every
18/// target.
19pub mod addon;
20pub mod builtins;
21
22/// The applier behind the `clipboard_write` and `clipboard_read` builtins,
23/// running on whichever clipboard backend the app was assembled with.
24pub mod clipboard;
25
26/// Host-neutral read side of the dynamic DOM API: `query` / `get_by_id` /
27/// traversal over the per-tick [`lumen_core::node::DomIndex`] snapshot.
28pub mod node_query;
29
30/// Host-neutral event object, binding registry, and propagation driver
31/// (phase 4): `n.on(type, handler)` / `off()`, the current-event cell, and
32/// the capture -> target -> bubble dispatch.
33pub mod event;
34
35/// Host-neutral low-level introspection read surface (phase 5): post-layout
36/// geometry, full computed style + provenance, typed component reads, tree
37/// serialization, and global runtime state.
38pub mod introspect;
39
40/// Runtime event dispatch (phase 4): input messages -> DOM events ->
41/// propagation, one pass per event over every installed host. See
42/// [`dom_events::deliver_input_events`].
43pub mod dom_events;
44
45/// Script languages as host modules register them, and the table that
46/// reaches an installed host. See [`language::ScriptLanguage`].
47pub mod language;
48
49/// Script-facing drag-and-drop event dispatchers (`on_drop` /
50/// `on_drag_start`). See the module docs.
51pub mod dnd;
52
53/// Builtins whose subsystem the running engine was linked without, and the
54/// once-per-builtin warning a call to one of them prints.
55pub mod unlinked;
56
57/// The HTTP seam behind `fetch()` / `http()`: the
58/// [`HttpClient`](http::HttpClient) trait and its request and response types,
59/// plus the [`HttpDispatch`](http::HttpDispatch) trait that decides who runs a
60/// request. `lumen-http-ureq` is the client Lumen ships.
61pub mod http;
62
63/// Host-generic ECS driver: dispatchers, derivation fixed-point, mirror
64/// sync, timers, HTTP fetch, and [`runtime::ScriptPlugin`]. Formerly the
65/// `lumen-script-runtime` crate; folded in with its flat surface
66/// re-exported at the crate root for source compatibility.
67pub mod runtime;
68
69/// The host-neutral native-function description ([`ScriptFn`]) and the
70/// app-wide channel plugins register them through.
71pub mod script_fn;
72
73/// The builtin surface every host shares, described once as [`ScriptFn`]s.
74pub mod builtin_fns;
75
76/// The `parse_json` and `parse_markdown` walks behind two of those builtins,
77/// each gated by its own Cargo feature. See the module docs.
78pub mod text_parse;
79
80/// The encoded form of the script surface: the version the shapes below are
81/// pinned at, and the adapters for the two property-store types
82/// [`ScriptCommand::SetProperty`] carries.
83pub mod wire;
84
85use std::collections::HashSet;
86
87use lumen_core::warn_line;
88use serde::{Deserialize, Serialize};
89use thiserror::Error;
90
91use crate::runtime::prefix;
92
93pub use builtin_fns::{builtin_script_fns, parse_dialog_filter_spec};
94pub use builtins::{BuiltinFn, BuiltinParam};
95pub use dnd::{dispatch_drag_start_to_script, dispatch_drops_to_script};
96pub use dom_events::{
97 EventHosts, PendingDomEvents, deliver_input_events, deliver_state_events,
98 queue_pointer_and_key_events, queue_state_events, register_dom_event_messages,
99 register_event_host,
100};
101pub use http::{
102 Credentials, DisabledHttpClient, HttpClient, HttpDispatch, HttpDone, HttpHook, HttpRequest,
103 HttpResponse, ThreadDispatch, UnknownCredentials,
104};
105pub use language::{
106 MarkupBlock, MarkupBlockError, ScriptCompile, ScriptHostAccess, ScriptLanguage,
107 ScriptLanguageAppExt, ScriptLanguages, ScriptProgram, UnknownLanguage, install_program,
108};
109pub use lumen_ir::source_map::SourceMap;
110pub use runtime::*;
111pub use script_fn::{
112 Arity0, CallScratch, IntoScriptFn, MAX_VARIADIC_ARITY, ScriptField, ScriptFn, ScriptFnAppExt,
113 ScriptFnBody, ScriptFnBuilder, ScriptFnCx, ScriptFnRegistry, ScriptFnStore, ScriptNs,
114 ScriptParam, ScriptPrelude, ScriptResult, ScriptRet, ScriptSig, ScriptStruct, ScriptTy,
115 ScriptType, with_call_scratch,
116};
117pub use wire::{PluginEvent, SCRIPT_WIRE_VERSION, push_plugin_event};
118
119/// Errors a [`ScriptHost`] can surface from `load` or `tick`.
120///
121/// Structured compile errors carry the source URI and the offending
122/// `(line, col)` so editor / LSP layers can place a squiggle without
123/// re-parsing the error message. `Runtime` is still a free-form string;
124/// Rhai's runtime errors don't always carry positions and the
125/// trait-level shape is "best effort".
126#[derive(Debug, Error)]
127pub enum ScriptError {
128 /// Source failed to parse / compile.
129 #[error("script compile error at {uri}:{line}:{col}: {message}")]
130 Compile {
131 /// Origin URI (e.g. `"main.cdl"` or `"lumen://app/main.cdl"`);
132 /// `"<inline>"` when the source has no associated URI.
133 uri: String,
134 /// 1-based line number reported by the parser. `0` when the
135 /// position is unknown.
136 line: u32,
137 /// 1-based column number reported by the parser. `0` when the
138 /// position is unknown.
139 col: u32,
140 /// Stringified parser message (sans position prefix).
141 message: String,
142 },
143 /// Source compiled but execution failed.
144 #[error("script runtime error: {0}")]
145 Runtime(String),
146}
147
148impl ScriptError {
149 /// Point a compile error in a concatenated program at the file and line
150 /// the author wrote.
151 ///
152 /// `uri` is the program's own name and `map` says where each of its
153 /// pieces was read from. An error about another file (a module the
154 /// program imports) already names that file and is returned unchanged,
155 /// as is a runtime error or one with no known line.
156 #[must_use]
157 pub fn relocate(self, uri: &str, map: &lumen_ir::source_map::SourceMap) -> Self {
158 match self {
159 Self::Compile {
160 uri: at,
161 line,
162 col,
163 message,
164 } if at == uri && line > 0 => match map.locate(line) {
165 Some((file, line)) if !file.is_empty() => Self::Compile {
166 uri: file.to_owned(),
167 line,
168 col,
169 message,
170 },
171 _ => Self::Compile {
172 uri: at,
173 line,
174 col,
175 message,
176 },
177 },
178 other => other,
179 }
180 }
181
182 /// Build a `Compile` with no specific position info. Equivalent to
183 /// the previous `Compile(String)` shape; used by backends that
184 /// can't surface line/col yet.
185 pub fn compile(message: impl Into<String>) -> Self {
186 Self::Compile {
187 uri: "<inline>".to_string(),
188 line: 0,
189 col: 0,
190 message: message.into(),
191 }
192 }
193}
194
195/// One side-effect a script wants to apply to the app this tick.
196///
197/// V1 is intentionally small: just enough to demo end-to-end. Future
198/// variants will cover `SetComponent`, `SpawnEntity`, `DespawnEntity`,
199/// `RegisterTimer`, etc., keyed by an entity id type that's stable across
200/// the script ABI boundary.
201///
202/// Variants are append-only; see [`SCRIPT_WIRE_VERSION`].
203#[derive(Debug, Clone, Serialize, Deserialize)]
204pub enum ScriptCommand {
205 /// Append a line to the app's diagnostic output.
206 Print(String),
207 /// Add `n` to the app's click counter (consumed in app-defined ways -
208 /// the host doesn't know about Clicks, it just emits this token).
209 AddClicks(i32),
210 /// Free-form key/value the app can interpret. Strings only for now.
211 SetString {
212 /// Key (e.g. `"title"`, `"label"`).
213 key: String,
214 /// Value.
215 value: String,
216 },
217 /// Replace the `TextContent` of the entity whose `LumenId` matches
218 /// `target_id` (set in markup via `id="..."`).
219 SetText {
220 /// Target entity id.
221 target_id: String,
222 /// New text content.
223 text: String,
224 },
225 /// Replace the asset path on an `<image>` entity. The runtime
226 /// strips the old `LoadedImage` / `LoadedSvg` / `ImageLoadFailed`
227 /// component, installs a fresh `ImageSource`, and the asset
228 /// pipeline re-decodes on the next tick. Used by scripts to swap
229 /// icons in response to runtime state (e.g. weather code ->
230 /// `icons/sun.png`).
231 SetSrc {
232 /// Target entity id.
233 target_id: String,
234 /// New asset path. Resolved relative to the app directory at
235 /// load time on the parser side; runtime mutations pass paths
236 /// through verbatim, so authors should compose relative paths
237 /// against their own app root (e.g. `"icons/sun.png"`).
238 path: String,
239 },
240 /// Schedule a named timer. The runtime calls the script's
241 /// `on_timer(name)` handler when it fires.
242 SetTimer {
243 /// Timer name, also passed as the `on_timer` argument.
244 name: String,
245 /// Delay until first fire, in milliseconds.
246 millis: u64,
247 /// If true, reschedule with the same interval after each fire.
248 repeat: bool,
249 },
250 /// Cancel a pending or repeating timer by name. No-op if absent.
251 CancelTimer {
252 /// Timer name to cancel.
253 name: String,
254 },
255 /// Issue an HTTP GET. The runtime spawns the request off-thread and
256 /// fires `on_fetch(tag, body)` when the response arrives.
257 ///
258 /// This is the simple sugar; [`Self::Http`] is the general form and
259 /// shares the same off-thread transport and completion channel.
260 Fetch {
261 /// URL to GET.
262 url: String,
263 /// Identifier the script gets back in `on_fetch(tag, body)`.
264 tag: String,
265 },
266 /// Issue a general HTTP request. The runtime performs it off-thread
267 /// (one worker per request) and, once the reply is marshalled back
268 /// onto the ECS/UI thread, fires `on_http(tag, response)` where
269 /// `response` is a map `#{ ok, status, headers, body, error }`.
270 ///
271 /// Request/reply shape is modeled on Qt's
272 /// `QNetworkRequest`/`QNetworkReply` (method + url + headers + body
273 /// in; status + headers + body + error out); the worker->UI-thread
274 /// marshalling before any signal is touched mirrors Slint's
275 /// `invoke_from_event_loop` discipline. `web`-`fetch`-like: a 4xx /
276 /// 5xx is a *completed* reply the script can branch on (via
277 /// `response.ok` / `response.status`), not an error.
278 Http {
279 /// HTTP method (`GET`, `POST`, `PUT`, `DELETE`, ...). Case-insensitive.
280 method: String,
281 /// Target URL.
282 url: String,
283 /// Request headers as ordered key/value pairs.
284 headers: Vec<(String, String)>,
285 /// Optional request body. `None` sends no body.
286 body: Option<String>,
287 /// Optional per-request timeout in milliseconds (`None` = no
288 /// client-imposed deadline).
289 timeout_ms: Option<u64>,
290 /// Whether the request carries the page's credentials. Only a
291 /// browser transport acts on it.
292 credentials: crate::http::Credentials,
293 /// Identifier echoed back in `on_http(tag, response)`.
294 tag: String,
295 },
296 /// Set the status code of the response the document is being rendered
297 /// for.
298 ///
299 /// A page rendered per request answers with more than markup: a page for
300 /// a record nobody has says so with a 404, and a form that rejects what
301 /// it was sent says so with a 422. Nothing outside a server render reads
302 /// this.
303 SetResponseStatus {
304 /// HTTP status code.
305 status: u16,
306 },
307 /// Set a header on the response the document is being rendered for.
308 ///
309 /// Setting the same name twice replaces the value. The renderer decides
310 /// what a script may set: a name that would rewrite how the body is
311 /// framed, or one carrying a line break, is refused and reported.
312 SetResponseHeader {
313 /// Header name.
314 name: String,
315 /// Header value.
316 value: String,
317 },
318 /// Answer the request with a redirect instead of a document.
319 ///
320 /// The render stops where it is: whatever the app had rendered is
321 /// dropped, because a visitor being sent elsewhere never sees it.
322 Redirect {
323 /// Where the visitor is sent, as a path or an absolute URL.
324 location: String,
325 },
326 /// Write `value` into the named slot of the reactive [`Signals`]
327 /// map. Any entity carrying a `Bind*` component pointing at this name
328 /// re-renders next tick. Strings only at this layer; numeric / bool
329 /// signals are stringified before storage.
330 SetSignal {
331 /// Signal name.
332 name: String,
333 /// New value.
334 value: String,
335 },
336 /// Typed write into [`lumen_core::property_store::PropertyStore`].
337 /// Unlike [`Self::SetSignal`], this variant carries a typed
338 /// [`PropertyValue`] (i64 / f64 / bool / color / vec2 / arc-str /
339 /// custom) and lands directly in the typed cell - no stringify, no
340 /// [`Signals`] mirror. Used by typed Rhai builtins
341 /// (`signal_set_int` / `_float` / `_bool` / `_color`) and the C
342 /// FFI typed setters when they want to enqueue through the script
343 /// command bus instead of the cross-thread `push_external_property`
344 /// channel. The Rhai host today prefers the channel path
345 /// (immediate, no tick round-trip); this variant is the
346 /// equivalent for hosts that want to defer until the next
347 /// `apply_script_commands` drain (gives the system one
348 /// frame to coalesce typed writes).
349 SetProperty {
350 /// Property key (global or entity-scoped).
351 #[serde(with = "crate::wire::property_key")]
352 key: lumen_core::property_store::PropertyKey,
353 /// New typed value. A
354 /// [`Custom`](lumen_core::property_store::PropertyValue::Custom)
355 /// payload cannot be encoded; see [`crate::wire::property_value`].
356 #[serde(with = "crate::wire::property_value")]
357 value: lumen_core::property_store::PropertyValue,
358 },
359 /// Replace the named entry in the reactive `ArraySignals` map with a
360 /// fresh ordered vector of records. Each record's field values are
361 /// pre-stringified at the script boundary so the receiving system
362 /// doesn't need to deserialize per-frame. Any `<for each="<name>">`
363 /// reconciler then spawns / despawns child markup to match.
364 SetArray {
365 /// Array name.
366 name: String,
367 /// Items, each a flat map of field -> stringified value.
368 items: Vec<std::collections::HashMap<String, String>>,
369 },
370 /// Show a native desktop notification. Cross-platform via
371 /// `notify-rust` (libnotify on Linux, NSUserNotification on macOS,
372 /// Toast on Windows).
373 Notify {
374 /// Notification summary / title.
375 title: String,
376 /// Body text.
377 body: String,
378 },
379 /// Show a native desktop notification carrying an icon, an urgency,
380 /// and action buttons. Pressing a button fires
381 /// `on_notification_action(id, action_id)`.
382 NotifyEx {
383 /// Stable id echoed back in `on_notification_action`.
384 id: String,
385 /// Notification summary / title.
386 title: String,
387 /// Body text.
388 body: String,
389 /// Settings as `"icon:name-or-path|urgency:critical"`. Unknown
390 /// keys are ignored; empty means no icon and normal urgency.
391 options: String,
392 /// Buttons as `"id:Label|id2:Label2"`. Empty means no buttons.
393 actions: String,
394 },
395 /// Put `text` on the system clipboard.
396 ClipboardWrite {
397 /// Text to copy.
398 text: String,
399 },
400 /// Request the system clipboard's text. The runtime reads it and
401 /// fires `on_clipboard(tag, text)`.
402 ClipboardRead {
403 /// Identifier the script gets back in the event handler.
404 tag: String,
405 },
406 /// Open a URL with the user's default handler (browser, mail client).
407 OpenUrl {
408 /// Absolute URL, including its scheme.
409 url: String,
410 },
411 /// Open a filesystem path with the platform's default application.
412 /// Relative paths resolve against the app directory.
413 OpenPath {
414 /// Path to open.
415 path: String,
416 },
417 /// Show a file in the platform's file manager. Relative paths resolve
418 /// against the app directory.
419 RevealPath {
420 /// Path to reveal.
421 path: String,
422 },
423 /// Hold off the screensaver and system sleep under `name` until a
424 /// matching [`ScriptCommand::AllowSleep`]. Repeating a live name
425 /// replaces its request rather than stacking a second one.
426 KeepAwake {
427 /// Identifier used to release the request later.
428 name: String,
429 /// Human-readable reason shown by the platform's power settings.
430 reason: String,
431 },
432 /// Release the sleep inhibit registered under `name`. No-op when the
433 /// name is unknown.
434 AllowSleep {
435 /// Identifier matching the previous `KeepAwake` call.
436 name: String,
437 },
438 /// Reads the PNG at `path` and writes its pixels to the system clipboard.
439 /// `path` is resolved relative to the app directory by the runtime handler.
440 CopyImageToClipboard {
441 /// Source PNG path.
442 path: String,
443 },
444 /// Pulls the current clipboard image (when present) and writes it as PNG to `path`. Failures log to stderr.
445 SaveClipboardImage {
446 /// Destination PNG path.
447 path: String,
448 },
449 /// Registers or replaces a system tray icon on macOS, Windows, and
450 /// Linux. `icon_path` resolves relative to the app dir.
451 RegisterTrayIcon {
452 /// Stable id; clicks fire `on_tray(id)`.
453 id: String,
454 /// Path to a PNG icon.
455 icon_path: String,
456 /// Optional hover tooltip.
457 tooltip: Option<String>,
458 /// Context-menu items as `"id:Label|-|id2:Label2"`, where `-` is a
459 /// separator. Picking one fires `on_menu(id)`. Empty means no menu.
460 menu: String,
461 /// macOS template-image flag: treat a monochrome icon as a
462 /// template and recolour it for the active menu-bar theme.
463 template: bool,
464 },
465 /// Drops a previously-registered tray icon by id.
466 UnregisterTrayIcon {
467 /// Matching id from `RegisterTrayIcon`.
468 id: String,
469 },
470 /// Replace the `LumenClasses` of the entity whose `LumenId`
471 /// matches `target_id` (or the root entity when `target_id` is
472 /// `"<root>"`). Triggers a runtime CSS re-apply (K9) - the
473 /// downstream system detects `Changed<LumenClasses>` on the root
474 /// and re-spawns the tree with the new class set.
475 SetClasses {
476 /// Target entity id, or the sentinel `<root>` to address the
477 /// markup root.
478 target_id: String,
479 /// Whitespace-separated class names - same shape as
480 /// `class="..."` markup attr.
481 classes: String,
482 },
483 /// Switch the app's color scheme. The runtime parses `name` into a
484 /// `lumen_core::components::ColorScheme` and hands it to the style
485 /// manager, which re-resolves every theme token. An unknown name is
486 /// logged and ignored.
487 SetColorScheme {
488 /// One of `"default"`, `"force-light"`, `"force-dark"`,
489 /// `"prefer-light"`, `"prefer-dark"`. `"default"` follows the OS.
490 name: String,
491 },
492 /// Open a native file dialog (open, multi-open, save, or folder
493 /// pick). The runtime serves the dialog on the main thread via
494 /// `rfd`, then fires `on_file_picked(tag, path)` /
495 /// `on_files_picked(tag, paths_joined_by_pipe)` /
496 /// `on_folder_picked(tag, path)` (or the per-id `on()` route)
497 /// once the user closes it.
498 OpenFileDialog {
499 /// What kind of dialog to show.
500 kind: FileDialogKind,
501 /// Identifier the script gets back in the event handler.
502 tag: String,
503 /// Filter list: `(label, extensions)` pairs. Empty = no
504 /// filter (show all files).
505 filters: Vec<(String, Vec<String>)>,
506 /// Default filename suggestion for `Save`. Ignored for the
507 /// other kinds.
508 default_name: Option<String>,
509 },
510 /// Register an OS-level global hotkey. Accelerator strings follow
511 /// the `keyboard-types`/Electron convention (`"CommandOrControl+S"`,
512 /// `"Alt+Space"`, `"F11"`). `on_hotkey(name)` fires every time the
513 /// OS dispatches the chord regardless of window focus.
514 RegisterHotkey {
515 /// Identifier the script gets back in `on_hotkey(name)`.
516 name: String,
517 /// Accelerator string. See `global-hotkey::HotKey::from_str`.
518 accelerator: String,
519 },
520 /// Unregister a previously-registered hotkey by name. No-op if
521 /// the name is unknown.
522 UnregisterHotkey {
523 /// Identifier matching the previous `RegisterHotkey` call.
524 name: String,
525 },
526 // -- dynamic DOM: change things (phase 2) --------------------------
527 /// Set an attribute on a node addressed by packed handle. KNOWN attrs
528 /// (`id`, `class`, `src`, `text`, `disabled`, ...) route to their typed
529 /// component; everything else lands in the generic
530 /// [`lumen_core::components::LumenAttributes`] map.
531 SetAttr {
532 /// Target node (packed handle or reserved spawn token).
533 node: u64,
534 /// Attribute name.
535 name: String,
536 /// New value.
537 value: String,
538 },
539 /// Remove an attribute from a node addressed by packed handle.
540 RemoveAttr {
541 /// Target node.
542 node: u64,
543 /// Attribute name to clear.
544 name: String,
545 },
546 /// Replace a node's text content (`node.set_text`).
547 SetNodeText {
548 /// Target node.
549 node: u64,
550 /// New text.
551 text: String,
552 },
553 /// Add one class to a node's class list (incremental, idempotent).
554 ClassAdd {
555 /// Target node.
556 node: u64,
557 /// Class name to add.
558 class: String,
559 },
560 /// Remove one class from a node's class list.
561 ClassRemove {
562 /// Target node.
563 node: u64,
564 /// Class name to remove.
565 class: String,
566 },
567 /// Toggle one class on a node's class list.
568 ClassToggle {
569 /// Target node.
570 node: u64,
571 /// Class name to toggle.
572 class: String,
573 },
574 /// Set an inline style property (`element.style`), the highest cascade
575 /// tier the runtime re-apply reads.
576 SetStyleProp {
577 /// Target node.
578 node: u64,
579 /// CSS property name.
580 name: String,
581 /// CSS value.
582 value: String,
583 },
584 /// Remove an inline style property.
585 RemoveStyleProp {
586 /// Target node.
587 node: u64,
588 /// CSS property name to clear.
589 name: String,
590 },
591
592 // -- dynamic DOM: build things (phase 3) ---------------------------
593 /// Create a fresh detached element with markup tag `tag`. `reserved`
594 /// is the token the host minted synchronously so the same tick's
595 /// chained mutations address the node; the applier maps it onto the
596 /// entity it spawns.
597 Spawn {
598 /// Markup tag (`div`, `button`, ...).
599 tag: String,
600 /// Reserved spawn token (see
601 /// [`lumen_core::node::reserve_node_token`]).
602 reserved: u64,
603 },
604 /// Attach `node` under `parent`. `before` (when non-zero) is a
605 /// reference child to insert ahead of (`insert_before`); zero appends
606 /// at the end (`append` / `set_parent` / `move_to` / `reparent` all
607 /// route here).
608 Insert {
609 /// Parent node.
610 parent: u64,
611 /// Node to attach (detaching it from any current parent).
612 node: u64,
613 /// Reference child to insert before, or `0` to append.
614 before: u64,
615 },
616 /// Replace `old` with `new` in `old`'s parent, then despawn `old`'s
617 /// subtree (`replaceWith`).
618 ReplaceWith {
619 /// Node being replaced (removed after).
620 old: u64,
621 /// Replacement node, moved into `old`'s slot.
622 new: u64,
623 },
624 /// Detach and despawn a node and its whole subtree (`node.remove`).
625 RemoveNode {
626 /// Node to remove.
627 node: u64,
628 },
629 /// Deep-clone `source`'s subtree into a fresh detached node. `reserved`
630 /// is the token the host minted for the clone root.
631 CloneNode {
632 /// Node to clone (with descendants).
633 source: u64,
634 /// Reserved spawn token for the clone root.
635 reserved: u64,
636 },
637 /// Replace `node`'s children with the subtree parsed from `markup`
638 /// (`element.innerHTML = ...`). The markup is parsed by the injected
639 /// front-end and spawned through the same path the `<for>` reconciler
640 /// uses, so layout / style / paint stay consistent.
641 ///
642 /// Guarded: parsing needs the injected markup front-end
643 /// ([`SourceParser`](crate) is a runtime concept), which is present on
644 /// the dev / from-source run path but absent in the precompiled-artifact
645 /// path; there it is a no-op. Do not feed untrusted content; this
646 /// injects live markup (XSS-adjacent).
647 SetInnerMarkup {
648 /// Target node whose children are replaced.
649 node: u64,
650 /// Markup fragment to parse and spawn as the new children.
651 markup: String,
652 },
653 /// Instantiate the compiled fragment `key` into a fresh detached
654 /// subtree. `reserved` is the token the host minted for the instance
655 /// root, so the same tick's `Insert` attaches it.
656 ///
657 /// `args` binds the fragment's parameters, in use-site order; a
658 /// parameter absent here takes its declared default, and one with
659 /// neither resolves empty. Arguments substitute once, at instantiation:
660 /// a value that changes while the app runs belongs in a `bind-*`
661 /// attribute inside the fragment body.
662 ///
663 /// `children` maps a slot name to a node that already exists, which the
664 /// applier moves into that slot. Passing live handles is what keeps the
665 /// applier off the script VM: a caller composing nested fragments
666 /// instantiates the inner ones first and passes their roots in.
667 SpawnFragment {
668 /// Key of the fragment to instantiate.
669 key: String,
670 /// Arguments as `(parameter name, value)` pairs.
671 args: Vec<(String, String)>,
672 /// Slot content as `(slot name, node handle)` pairs.
673 children: Vec<(String, u64)>,
674 /// Reserved spawn token for the instance root.
675 reserved: u64,
676 },
677
678 // -- dynamic DOM: events (phase 4) ---------------------------------
679 /// Bind an event handler to a node. `token` is the off token the host
680 /// minted synchronously; the host holds the handler closure keyed by
681 /// the same token, and the applier records `(node, event_type, capture)`
682 /// in the host-neutral binding registry the dispatcher consults.
683 /// `handler_ref` is the token (a host-neutral reference), consistent
684 /// with the reserved-token model the structural commands use.
685 BindEvent {
686 /// Target node (packed handle or reserved spawn token).
687 node: u64,
688 /// Event type (`"click"`, `"keydown"`, ...).
689 event_type: String,
690 /// `true` for a capture-phase listener.
691 capture: bool,
692 /// Off / handler token (`handler_ref`).
693 token: u64,
694 },
695 /// Unbind a previously-bound event handler by its off token
696 /// (`removeEventListener`). No-op if the token is unknown.
697 UnbindEvent {
698 /// Off token returned by the matching `on(...)`.
699 token: u64,
700 },
701
702 // -- window / document (section 4.8) -------------------------------
703 /// Set the OS window title (`window.set_title`).
704 WindowSetTitle {
705 /// New title string.
706 title: String,
707 },
708 /// Resize the OS window to `width` x `height` logical pixels
709 /// (`window.set_size`).
710 WindowSetSize {
711 /// Logical width.
712 width: f32,
713 /// Logical height.
714 height: f32,
715 },
716
717 // -- app lifecycle (recent files, autostart) ------------------------
718 /// Record `path` as recently opened (move-to-front, capped list).
719 /// Relative paths resolve against the app directory.
720 AddRecentFile {
721 /// File path to record.
722 path: String,
723 /// Display label; empty derives one from the path's file name.
724 label: String,
725 },
726 /// Request the recent-files list. The runtime reads it and fires
727 /// `on_recent_files(tag, paths)`, most recent first, paths joined by
728 /// `|`.
729 ListRecentFiles {
730 /// Identifier the script gets back in the event handler.
731 tag: String,
732 },
733 /// Remove every entry from the recent-files list.
734 ClearRecentFiles,
735 /// Enable or disable launching this app at login.
736 SetAutostart {
737 /// Whether the app should autostart.
738 on: bool,
739 },
740 /// Request the current autostart state. The runtime reads it and fires
741 /// `on_autostart_enabled(tag)` or `on_autostart_disabled(tag)`.
742 QueryAutostart {
743 /// Identifier the script gets back in the event handler.
744 tag: String,
745 },
746 /// Ask for one `on_frame(dt)` call on the next tick.
747 ///
748 /// The browser's `requestAnimationFrame`: one request buys one callback,
749 /// and a callback that wants another asks again. An app that stops asking
750 /// stops being ticked for it, so an animation costs frames only while it
751 /// is running.
752 RequestFrame,
753 /// Switch the app to another locale. The catalogue, the locale-aware
754 /// formatters and the base writing direction all move together, and
755 /// every element the markup marked is rebuilt from the new catalogue on
756 /// the tick the switch lands.
757 ///
758 /// A locale the app has no catalogue for is not an error: every message
759 /// falls back to the text the author wrote, which is what naming such a
760 /// locale in `lumen.toml` already does. A `tag` that is not BCP-47 is
761 /// logged and ignored.
762 SetLocale {
763 /// BCP-47 tag, such as `"de-DE"`.
764 tag: String,
765 },
766}
767
768impl ScriptCommand {
769 /// Whether the command changes the tree rather than the state behind it,
770 /// which is what the DOM applier answers for.
771 pub fn mutates_dom(&self) -> bool {
772 matches!(
773 self,
774 ScriptCommand::SetAttr { .. }
775 | ScriptCommand::RemoveAttr { .. }
776 | ScriptCommand::SetNodeText { .. }
777 | ScriptCommand::SetClasses { .. }
778 | ScriptCommand::ClassAdd { .. }
779 | ScriptCommand::ClassRemove { .. }
780 | ScriptCommand::ClassToggle { .. }
781 | ScriptCommand::SetStyleProp { .. }
782 | ScriptCommand::RemoveStyleProp { .. }
783 | ScriptCommand::Spawn { .. }
784 | ScriptCommand::Insert { .. }
785 | ScriptCommand::ReplaceWith { .. }
786 | ScriptCommand::RemoveNode { .. }
787 | ScriptCommand::CloneNode { .. }
788 | ScriptCommand::SetInnerMarkup { .. }
789 | ScriptCommand::SpawnFragment { .. }
790 | ScriptCommand::BindEvent { .. }
791 | ScriptCommand::UnbindEvent { .. }
792 | ScriptCommand::WindowSetTitle { .. }
793 | ScriptCommand::WindowSetSize { .. }
794 )
795 }
796
797 /// Whether the command adds, removes or moves a node, rather than writing
798 /// onto one the markup already declares.
799 ///
800 /// A node a script builds is built by running the script, so anything
801 /// producing a document without running one for the full life of the app
802 /// has to know that these went by: a page rendered on a server carries the
803 /// markup, the state and what the run wrote onto the nodes it found, and
804 /// the nodes a script went on to create arrive when the browser runs it.
805 pub fn builds_nodes(&self) -> bool {
806 matches!(
807 self,
808 ScriptCommand::Spawn { .. }
809 | ScriptCommand::Insert { .. }
810 | ScriptCommand::ReplaceWith { .. }
811 | ScriptCommand::RemoveNode { .. }
812 | ScriptCommand::CloneNode { .. }
813 | ScriptCommand::SetInnerMarkup { .. }
814 | ScriptCommand::SpawnFragment { .. }
815 )
816 }
817}
818
819/// The builtins that open a file dialog, as the prefixes a source scan looks
820/// for: `pick_file` also covers `pick_files` and `pick_file_filtered`. What
821/// installs a dialog executor, and what compiles one in, both key off these.
822pub const FILE_DIALOG_BUILTINS: &[&str] = &["pick_file", "save_file", "pick_folder"];
823
824/// File dialog flavour for [`ScriptCommand::OpenFileDialog`].
825#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
826pub enum FileDialogKind {
827 /// Pick one existing file. Fires `on_file_picked(tag, path)`.
828 Open,
829 /// Pick multiple existing files. Fires
830 /// `on_files_picked(tag, "p1|p2|p3")` (paths joined with `|`).
831 OpenMulti,
832 /// Pick a save destination (may not exist yet). Fires
833 /// `on_file_picked(tag, path)` (same handler as `Open` since the
834 /// shape is identical).
835 Save,
836 /// Pick a folder. Fires `on_folder_picked(tag, path)`.
837 PickFolder,
838}
839
840/// Typed sum the script-host trait understands across the boundary.
841///
842/// Mirrors what a dynamic script value carries - scalars, arrays, maps -
843/// without leaking any host's value type into the trait. Backends translate
844/// to / from their native value type.
845///
846/// Variants are append-only; see [`SCRIPT_WIRE_VERSION`].
847#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
848pub enum ScriptValue {
849 /// Absence of a value (Rhai `()`).
850 Unit,
851 /// Boolean payload.
852 Bool(bool),
853 /// Signed 64-bit integer payload.
854 I64(i64),
855 /// 64-bit float payload.
856 F64(f64),
857 /// Owned string payload.
858 Str(String),
859 /// Heterogeneous list.
860 Array(Vec<ScriptValue>),
861 /// Field map (insertion order is not preserved across the boundary).
862 Map(std::collections::HashMap<String, ScriptValue>),
863}
864
865impl From<bool> for ScriptValue {
866 fn from(v: bool) -> Self {
867 Self::Bool(v)
868 }
869}
870
871impl From<i64> for ScriptValue {
872 fn from(v: i64) -> Self {
873 Self::I64(v)
874 }
875}
876
877impl From<f64> for ScriptValue {
878 fn from(v: f64) -> Self {
879 Self::F64(v)
880 }
881}
882
883impl From<String> for ScriptValue {
884 fn from(v: String) -> Self {
885 Self::Str(v)
886 }
887}
888
889impl<'a> From<&'a str> for ScriptValue {
890 fn from(v: &'a str) -> Self {
891 Self::Str(v.to_string())
892 }
893}
894
895impl ScriptValue {
896 /// Stringify any variant. Strings stay verbatim; everything else
897 /// uses the canonical `Display` form. UNIT becomes the empty string.
898 pub fn stringify(&self) -> String {
899 match self {
900 Self::Unit => String::new(),
901 Self::Bool(b) => b.to_string(),
902 Self::I64(i) => i.to_string(),
903 Self::F64(f) => f.to_string(),
904 Self::Str(s) => s.clone(),
905 Self::Array(arr) => {
906 let parts: Vec<String> = arr.iter().map(Self::stringify).collect();
907 format!("[{}]", parts.join(", "))
908 }
909 Self::Map(m) => {
910 let mut keys: Vec<&String> = m.keys().collect();
911 keys.sort();
912 let parts: Vec<String> = keys
913 .iter()
914 .map(|k| format!("{}: {}", k, m[*k].stringify()))
915 .collect();
916 format!("{{{}}}", parts.join(", "))
917 }
918 }
919 }
920}
921
922/// Backend-agnostic facade exposing the reactive property store to
923/// script hosts. Mirrors QML's `QQmlContext::setContextProperty` /
924/// `contextProperty()` shape: scripts read + write through a thin layer
925/// the host keeps internally so callers don't have to thread an
926/// `Engine` or a per-backend `Dynamic` type through.
927///
928/// Concrete impls own the underlying signal store. The trait is intentionally small:
929/// every method maps to one host-side write so backends can rebuild
930/// their internal mirror without re-implementing scalar/array policy
931/// on top of `Dynamic`.
932pub trait ScriptContext {
933 /// Read a named scalar / structured property. `None` => the property
934 /// has never been written.
935 fn get(&self, name: &str) -> Option<ScriptValue>;
936
937 /// Write a named scalar / structured property. The backend mirrors
938 /// the write into its native value type and queues a stringified
939 /// `SetSignal` for the ECS-side `Signals` resource.
940 fn set(&mut self, name: &str, value: ScriptValue);
941
942 /// Append `value` onto the named array property. Lazy-initialises
943 /// the array if absent. Drives `<for each="<name>">` reconciliation.
944 fn array_push(&mut self, name: &str, value: ScriptValue);
945
946 /// Clear the named array property (set to empty).
947 fn array_clear(&mut self, name: &str);
948
949 /// Entity-scoped read for properties keyed by `(entity_id, name)`.
950 /// Returns `None` when the backend doesn't model entity-scoped
951 /// properties (most backends do not).
952 fn entity(&self, entity_id: &str, name: &str) -> Option<ScriptValue> {
953 let _ = (entity_id, name);
954 None
955 }
956}
957
958/// Outcome of one [`ScriptHost::call`] invocation.
959///
960/// `commands` is the full drain of the host's command sink at return. It is
961/// drained even when `found == false`, because builtins invoked outside
962/// handlers may have queued commands.
963#[derive(Debug, Clone)]
964pub struct CallOutcome {
965 /// Commands the host's builtins pushed into the sink during (and
966 /// before) the call, drained at return.
967 pub commands: Vec<ScriptCommand>,
968 /// The script function's return value. `None` when the function was
969 /// not found (or no program is loaded). Carries meaning for hooks
970 /// like `on_close()` -> `Bool(false)` vetoes the close.
971 pub ret: Option<ScriptValue>,
972 /// `false` when the function does not exist - the runtime probes
973 /// optional handlers and treats a miss as silent success.
974 pub found: bool,
975}
976
977/// A [`ScriptHost::call`] whose function raised an error.
978///
979/// `commands` holds what the function queued before it failed, drained from
980/// the sink like a successful call's. The runtime applies them, the way a
981/// browser keeps the DOM changes a throwing handler made before the throw:
982/// a `print` placed before the failing line still reaches the log, and a
983/// timer armed there still fires.
984#[derive(Debug)]
985pub struct CallFailure {
986 /// The error the function raised.
987 pub error: ScriptError,
988 /// Commands queued before the failure.
989 pub commands: Vec<ScriptCommand>,
990}
991
992impl std::fmt::Display for CallFailure {
993 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
994 self.error.fmt(f)
995 }
996}
997
998impl std::error::Error for CallFailure {
999 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
1000 Some(&self.error)
1001 }
1002}
1003
1004/// Merge a pre-reload registry snapshot back under the entries the reloaded
1005/// script just registered, so a reload never silently loses a registration.
1006///
1007/// `current` holds what the new top level registered and wins every collision;
1008/// `prior` fills in the rest. Every [`ScriptHost::replace`] implementation runs
1009/// this over its handler, event-handler, and derivation maps on the success
1010/// path. The registrations that need it are the ones an app makes from
1011/// `on_start`: that callback fires once at app construction and never again, so
1012/// its handlers cannot re-register themselves during a reload.
1013///
1014/// One case stays stale until the app restarts: renaming a handler function
1015/// together with the `on(...)` call that binds it leaves the old name in the
1016/// map under its old key, because nothing in the new source collides with it.
1017pub fn carry_forward<K, V>(
1018 current: &mut std::collections::HashMap<K, V>,
1019 prior: std::collections::HashMap<K, V>,
1020) where
1021 K: std::hash::Hash + Eq,
1022{
1023 for (key, value) in prior {
1024 current.entry(key).or_insert(value);
1025 }
1026}
1027
1028/// Backend that compiles + executes scripts and exposes the host-neutral
1029/// registries the generic runtime (`lumen-script`) drives: the
1030/// command sink, the rich-typed signal mirror, the per-id handler
1031/// registry, and the derivation registry.
1032///
1033/// `Send + Sync` bound: hosts sit in a plain bevy `Resource` and
1034/// participate in parallel scheduling. A host that cannot be `Send` needs a
1035/// `NonSend` variant of the install path.
1036///
1037/// One script-side callable the generic runtime can re-invoke is modeled
1038/// by [`Self::Closure`] - Rhai: `FnPtr`; candela: a function handle once its
1039/// embedding API lands.
1040pub trait ScriptHost: Send + Sync + 'static {
1041 /// Host-native handle to a script closure (a `derive(...)` body).
1042 type Closure: Clone + Send + Sync + 'static;
1043
1044 /// Whether this host is the one a component marker in the tree names a
1045 /// function in.
1046 ///
1047 /// A marker is what the build leaves where a component has to run, and
1048 /// markup blocks are one language's macro, so exactly one host answers
1049 /// for them. Every other host leaves a marker alone rather than reporting
1050 /// a function it was never going to have.
1051 const FILLS_COMPONENTS: bool = false;
1052
1053 // -- lifecycle -----------------------------------------------------
1054
1055 /// Compile-only, side-effect free, with the exact engine settings
1056 /// [`Self::load`] uses - `lumenc check` and `run` must agree on what
1057 /// parses. `uri` seeds [`ScriptError::Compile::uri`].
1058 fn compile_check(&self, source: &str, uri: &str) -> Result<(), ScriptError>;
1059
1060 /// Compile + evaluate the top level into persistent state.
1061 /// Fresh-start load; replaces any previously loaded program.
1062 fn load(&mut self, source: &str, uri: &str) -> Result<(), ScriptError>;
1063
1064 /// Hot reload: compile first (no state touched on a parse error), then
1065 /// swap atomically. Snapshot the registries, clear them, re-evaluate the
1066 /// top level into the existing persistent scope, and roll the snapshot
1067 /// back in full if that evaluation fails. Persistent across the swap:
1068 /// engine registrations, scope bindings, the signal mirror, and the
1069 /// in-flight command sink.
1070 ///
1071 /// On success the snapshot is merged back under whatever the new top
1072 /// level registered, via [`carry_forward`]. Lifecycle callbacks do not
1073 /// re-fire on reload, so a handler an app bound from `on_start` has no
1074 /// second chance to register itself; without the merge every such handler
1075 /// would go quiet after the first reload.
1076 fn replace(&mut self, source: &str, uri: &str) -> Result<(), ScriptError>;
1077
1078 /// Drop the loaded program and all persistent state. Genuine
1079 /// restart; hot reload should use [`Self::replace`].
1080 fn reset(&mut self);
1081
1082 // -- invocation (reactive-only: no per-frame hook in this trait) ---
1083
1084 /// Call a script function by name. A missing function is silent
1085 /// success (`found: false`); see [`CallOutcome`]. A function that raises
1086 /// returns [`CallFailure`], which still carries the commands it queued
1087 /// before the error.
1088 fn call(&mut self, fn_name: &str, args: &[ScriptValue]) -> Result<CallOutcome, CallFailure>;
1089
1090 /// Re-entrant closure invocation. Must be callable while the generic
1091 /// runtime holds NO host locks (snapshot-then-call pattern): the
1092 /// closure body may re-enter builtins that touch the sink / mirror.
1093 fn call_closure(
1094 &mut self,
1095 closure: &Self::Closure,
1096 args: &[ScriptValue],
1097 ) -> Result<ScriptValue, ScriptError>;
1098
1099 /// Evaluate one derivation: read the dep values, invoke `closure`,
1100 /// store the rich result into the mirror under `name`, and return
1101 /// the host-canonical stringification for the ECS property store.
1102 ///
1103 /// Deviation from the original v2 sketch (documented in the design
1104 /// doc section 1.9): the default impl composes `mirror_get` -> `call_closure`
1105 /// -> `mirror_set` -> [`ScriptValue::stringify`], but hosts should
1106 /// override with a native path - round-tripping dep values through
1107 /// [`ScriptValue`] loses fidelity for host-native structured values,
1108 /// and stringification is host-canonical (Rhai renders `1.0` where
1109 /// Rust's `f64` Display renders `1`; store strings feed `bind-text`
1110 /// and the parse-back policy, so the host must own the format).
1111 fn eval_derivation(
1112 &mut self,
1113 closure: &Self::Closure,
1114 deps: &[String],
1115 name: &str,
1116 ) -> Result<String, ScriptError> {
1117 let args: Vec<ScriptValue> = deps
1118 .iter()
1119 .map(|d| self.mirror_get(d).unwrap_or(ScriptValue::Unit))
1120 .collect();
1121 let value = self.call_closure(closure, &args)?;
1122 let text = value.stringify();
1123 self.mirror_set(name, value);
1124 Ok(text)
1125 }
1126
1127 // -- command sink --------------------------------------------------
1128
1129 /// Drain commands queued by builtins since the last drain. Default: a
1130 /// host with no command sink has nothing to drain.
1131 fn drain_commands(&mut self) -> Vec<ScriptCommand> {
1132 Vec::new()
1133 }
1134
1135 /// Put commands back into the sink so they flow through the next
1136 /// tick's normal drain (the `on_start` re-stash). Default: dropped.
1137 fn push_commands(&mut self, cmds: Vec<ScriptCommand>) {
1138 let _ = cmds;
1139 }
1140
1141 // -- signal mirror (host-local, rich-typed) ------------------------
1142
1143 /// Read a mirror entry. `None` => never written. Default: no mirror.
1144 fn mirror_get(&self, name: &str) -> Option<ScriptValue> {
1145 let _ = name;
1146 None
1147 }
1148
1149 /// Write a mirror entry (host-native conversion of `value`). Default:
1150 /// no mirror, so the write is dropped.
1151 fn mirror_set(&mut self, name: &str, value: ScriptValue) {
1152 let _ = (name, value);
1153 }
1154
1155 /// section 1.3 type-preserving parse-back of a store string. The trait pins
1156 /// the POLICY: a mirror entry currently holding a scalar (bool / int
1157 /// / float) parses the string back into that SAME type; structured
1158 /// mirror values (arrays, maps) stay authoritative and ignore the
1159 /// string; unparseable strings leave the mirror untouched; absent /
1160 /// string entries take the store string verbatim. Default: no mirror.
1161 fn mirror_sync_str(&mut self, name: &str, value: &str) {
1162 let _ = (name, value);
1163 }
1164
1165 // -- registries populated by host builtins, read by the runtime ----
1166
1167 /// Per-id handler lookup (`on(event, id, fn)`), including the
1168 /// template-suffix fallback: a handler registered for `save` also
1169 /// matches `user-card:save` via the last-`:` suffix. Default: no
1170 /// handler registry.
1171 fn handler_for(&self, event: &str, key: &str) -> Option<String> {
1172 let _ = (event, key);
1173 None
1174 }
1175
1176 /// Snapshot of derivations matching `dirty` plus `pending`
1177 /// `(name, deps, closure)` - taken OUTSIDE any host lock so the
1178 /// driver can invoke closures re-entrantly. Default: no derivation
1179 /// registry.
1180 fn derivations_matching(
1181 &self,
1182 dirty: &HashSet<&str>,
1183 pending: &HashSet<String>,
1184 ) -> Vec<(String, Vec<String>, Self::Closure)> {
1185 let _ = (dirty, pending);
1186 Vec::new()
1187 }
1188
1189 /// Names of derivations registered but never successfully evaluated;
1190 /// they all run on the next derivation pass regardless of dirt.
1191 /// Default: none.
1192 fn pending_initial(&self) -> HashSet<String> {
1193 HashSet::new()
1194 }
1195
1196 /// Remove successfully-evaluated names from the pending-initial set.
1197 /// Erroring derivations stay pending and retry next tick. Default: no-op.
1198 fn clear_pending(&mut self, evaluated: &[String]) {
1199 let _ = evaluated;
1200 }
1201
1202 // -- event handlers (phase 4) --------------------------------------
1203
1204 /// Invoke the event-handler closure the host registered under `token`
1205 /// (via its `on(node, type, handler)` builtin). The host builds its
1206 /// native event object, which reads the process-global current-event
1207 /// cell ([`crate::event`]); commands the handler queues are drained via
1208 /// [`Self::drain_commands`] afterwards by the caller. Returns whether a
1209 /// closure was found and run.
1210 ///
1211 /// Default: not supported (no closure registry) -> `Ok(false)`. Hosts
1212 /// that expose `on(...)` with real closures override this.
1213 fn dispatch_event_handler(&mut self, token: u64) -> Result<bool, ScriptError> {
1214 let _ = token;
1215 Ok(false)
1216 }
1217
1218 /// Drop the event-handler closure registered under `token` (the host
1219 /// side of `off()` / `UnbindEvent`). Default: no-op.
1220 fn drop_event_handler(&mut self, token: u64) {
1221 let _ = token;
1222 }
1223
1224 // -- extension -----------------------------------------------------
1225
1226 /// Bind one [`ScriptFn`] so the script can call it.
1227 ///
1228 /// The host maps [`ScriptNs`] onto its own name space, binds the
1229 /// signature's arity range, and retains `f` in a [`ScriptFnStore`] so
1230 /// [`Self::reset`] can put it back. A host's own engine accessors
1231 /// remain available for anything this shape cannot express.
1232 ///
1233 /// # Errors
1234 ///
1235 /// The host could not bind the function: its binding window has closed
1236 /// (candela binds host declarations when the artifact loads) or the engine
1237 /// rejected the registration.
1238 fn register_script_fn(&mut self, f: &ScriptFn) -> Result<(), ScriptError>;
1239
1240 /// Take source a plugin ships alongside the functions it registered under
1241 /// `ns`, to be compiled ahead of the app's own program.
1242 ///
1243 /// This is how a plugin offers sugar over its functions: a struct and an
1244 /// `impl` block in this host's own language, so a script calls
1245 /// `pin(21).level()` instead of the free function. The registry hands a
1246 /// host only the sources written in its language.
1247 ///
1248 /// A host that cannot stage source says so rather than dropping it, which
1249 /// is what the default does.
1250 fn add_prelude(&mut self, ns: &str, source: &str) {
1251 let _ = source;
1252 warn_line!(
1253 "{}: the `{ns}` plugin ships source this host cannot compile ahead of the program; \
1254 call its functions directly",
1255 prefix(self.lang())
1256 );
1257 }
1258
1259 // -- metadata ------------------------------------------------------
1260
1261 /// Language tag, such as `"candela"`. Used in diagnostics prefixes
1262 /// (`lumen-<lang>: ...`).
1263 fn lang(&self) -> &'static str;
1264
1265 /// The builtin-function metadata table feeding LSP completion /
1266 /// hover and the parity test. Default: none.
1267 fn builtins(&self) -> &'static [BuiltinFn] {
1268 &[]
1269 }
1270}
1271
1272/// Marker trait reserving the script-side state-proxy contract. Currently empty.
1273pub trait StateProxy {}