Skip to main content

konjure_wasm/
language.rs

1//! Browser boundary for the persistent language runtime and source services.
2use konjure_lang::inspection::{InspectionError, InspectionTarget};
3use konjure_lang::{
4    Value,
5    live::{LiveCommand, LiveEvent, LiveHistory, LiveRuntime as CoreLiveRuntime},
6    spatial::SpatialRuntime,
7};
8use konjure_sdk::presentation::UiEvent;
9use std::collections::BTreeMap;
10use wasm_bindgen::prelude::*;
11
12const MAX_MODULE_BYTES: usize = 1024 * 1024;
13
14/// Validates semantic UI and routes it through the supplied device topology.
15/// Unresolved world placement is rejected; adapters must supply tracking elsewhere.
16#[wasm_bindgen]
17pub fn presentation_plan(document_json: &str, target_json: &str) -> Result<String, JsValue> {
18    use konjure_sdk::presentation::{
19        DeviceTarget, PresentationContext, PresentationPlan, UiDocument,
20    };
21    if document_json.len() > 1024 * 1024 || target_json.len() > 4096 {
22        return Err(JsValue::from_str("presentation input exceeds its bound"));
23    }
24    let document: UiDocument = serde_json::from_str(document_json).map_err(super::json_error)?;
25    let target: DeviceTarget = serde_json::from_str(target_json).map_err(super::json_error)?;
26    let plan = PresentationPlan::build(&document, &target, &PresentationContext::default())
27        .map_err(|error| JsValue::from_str(&error.to_string()))?;
28    super::encode(&plan)
29}
30
31/// A checked participant join URL rendered as a camera-readable SVG by Rust.
32#[wasm_bindgen]
33pub fn live_pairing_qr_svg(join_url: &str) -> Result<String, JsValue> {
34    konjure_lang::live::pairing::qr_svg(join_url)
35        .map_err(|error| JsValue::from_str(&error.to_string()))
36}
37
38/// Parses a pasted participant invitation without joining a room or authorizing transport.
39/// The returned JSON is the same validated pairing link accepted from a native QR scan.
40#[wasm_bindgen]
41pub fn live_pairing_parse_json(join_url: &str) -> Result<String, JsValue> {
42    let link = konjure_lang::live::pairing::PairingLink::parse(join_url)
43        .map_err(|error| JsValue::from_str(&error.to_string()))?;
44    super::encode(&link)
45}
46
47fn modules(json: &str) -> Result<BTreeMap<String, String>, JsValue> {
48    if json.len() > MAX_MODULE_BYTES {
49        return Err(JsValue::from_str("module bundle exceeds 1 MiB"));
50    }
51    serde_json::from_str(json).map_err(super::json_error)
52}
53fn error(value: &impl serde::Serialize) -> JsValue {
54    JsValue::from_str(
55        &serde_json::to_string(value)
56            .unwrap_or_else(|_| "language diagnostic encoding failed".into()),
57    )
58}
59
60/// Standard names, signatures and reference URLs, generated from the Rust catalog.
61#[wasm_bindgen]
62pub fn language_catalog() -> Result<String, JsValue> {
63    super::encode(&konjure_lang::catalog::catalog())
64}
65
66/// Analyze source without running initializers, systems, or native effects.
67/// Every offset is a UTF-8 byte offset in the indicated source module.
68#[wasm_bindgen]
69pub fn language_analyze(source: &str, modules_json: &str) -> Result<String, JsValue> {
70    let supplied = modules(modules_json)?;
71    let (symbols, methods, references, diagnostics) = match konjure_lang::compile(source, &supplied) {
72        Ok(program) => (program.metadata().symbols.iter().map(|s| serde_json::json!({
73            "name":s.name, "kind":s.kind, "start":s.span.start, "end":s.span.end,
74            "module":s.span.module, "signature":s.signature, "qualified_name":s.qualified_name,
75        })).collect::<Vec<_>>(), program.metadata().methods.clone(), program.metadata().references.clone(), Vec::new()),
76        Err(diagnostics) => (Vec::new(), Vec::new(), Vec::new(), diagnostics),
77    };
78    let tokens = konjure_lang::parser::tokenize(source, "main").unwrap_or_default();
79    super::encode(&serde_json::json!({
80        "tokens":tokens.iter().map(|t| serde_json::json!({
81            "start":t.span.start,"end":t.span.end,"kind":t.kind,"text":t.text,
82        })).collect::<Vec<_>>(), "symbols":symbols, "methods":methods, "references":references, "diagnostics":diagnostics,
83    }))
84}
85
86/// Persistent, explicitly stepped interpreter and physics world.
87/// Call `free()` when replacing a program or disposing its editor.
88#[wasm_bindgen]
89pub struct LanguageRuntime {
90    inner: SpatialRuntime,
91}
92
93/// Bounded replayable live-session protocol. The browser transports only JSON;
94/// compilation, callbacks, simulation, and render validation remain in Rust.
95#[wasm_bindgen]
96#[derive(Default)]
97pub struct LiveRuntime {
98    inner: CoreLiveRuntime,
99}
100
101#[wasm_bindgen]
102impl LiveRuntime {
103    /// Reads semantic controls; source revision and callbacks are owned by Rust.
104    pub fn ui_json(&self) -> Result<String, JsValue> {
105        super::encode(&self.inner.ui_document().map_err(|value| error(&value))?)
106    }
107    /// Creates an empty session. Apply a `load` command before stepping or rendering.
108    #[wasm_bindgen(constructor)]
109    pub fn new() -> Self {
110        Self {
111            inner: CoreLiveRuntime::new(),
112        }
113    }
114
115    /// Applies one command and returns the assigned sequence event as JSON.
116    pub fn apply_json(&mut self, command_json: &str) -> Result<String, JsValue> {
117        if command_json.len() > 4 * 1024 * 1024 {
118            return Err(JsValue::from_str("live command exceeds 4 MiB"));
119        }
120        let command: LiveCommand = serde_json::from_str(command_json).map_err(super::json_error)?;
121        super::encode(&self.inner.apply(command).map_err(|value| error(&value))?)
122    }
123
124    /// Validates and applies a semantic control event for a locally hosted or detached session.
125    /// Connected clients must send UI input to the live authority, which orders the command.
126    pub fn apply_ui_json(&mut self, event_json: &str) -> Result<String, JsValue> {
127        if event_json.len() > 8 * 1024 {
128            return Err(JsValue::from_str("UI event exceeds 8 KiB"));
129        }
130        let event: UiEvent = serde_json::from_str(event_json).map_err(super::json_error)?;
131        let command = self
132            .inner
133            .ui_command(&event)
134            .map_err(|value| error(&value))?;
135        super::encode(&self.inner.apply(command).map_err(|value| error(&value))?)
136    }
137
138    /// Accepts one exact-next replay event. Gaps and duplicates preserve state.
139    pub fn accept_json(&mut self, event_json: &str) -> Result<(), JsValue> {
140        if event_json.len() > 4 * 1024 * 1024 {
141            return Err(JsValue::from_str("live event exceeds 4 MiB"));
142        }
143        let event: LiveEvent = serde_json::from_str(event_json).map_err(super::json_error)?;
144        self.inner.accept(event).map_err(|value| error(&value))
145    }
146
147    /// Replaces this session with a transactionally replayed history.
148    pub fn restore_json(&mut self, history_json: &str) -> Result<(), JsValue> {
149        if history_json.len() > konjure_lang::live::MAX_JOURNAL_BYTES {
150            return Err(JsValue::from_str("live history exceeds 16 MiB"));
151        }
152        let history: LiveHistory = serde_json::from_str(history_json).map_err(super::json_error)?;
153        self.inner.restore(history).map_err(|value| error(&value))
154    }
155
156    /// Returns the retained event suffix and its base sequence as JSON.
157    pub fn history_json(&self) -> Result<String, JsValue> {
158        super::encode(self.inner.history())
159    }
160
161    /// Returns the accepted snapshot, or JSON `null` before the first load.
162    pub fn snapshot_json(&self) -> Result<String, JsValue> {
163        super::encode(&self.inner.snapshot())
164    }
165
166    /// Returns validated generated render data for the accepted revision.
167    pub fn render_json(&self) -> Result<String, JsValue> {
168        super::encode(&self.inner.render().map_err(|value| error(&value))?)
169    }
170
171    /// Inspects accepted tensor data without executing source or mutating the session.
172    pub fn inspect_tensor_json(&self, target_json: &str) -> String {
173        let result = if target_json.len() > 4096 {
174            Err(InspectionError::InvalidTarget)
175        } else {
176            self.inner
177                .snapshot()
178                .ok_or(InspectionError::InvalidTarget)
179                .and_then(|snapshot| {
180                    serde_json::from_str::<InspectionTarget>(target_json)
181                        .map_err(|_| InspectionError::InvalidTarget)
182                        .and_then(|target| snapshot.tensor_inspection(&target))
183                })
184        };
185        serde_json::to_string(&result).unwrap_or_else(|_| "{\"Err\":\"encoding\"}".into())
186    }
187}
188
189#[wasm_bindgen]
190impl LanguageRuntime {
191    /// Reads semantic controls for this standalone interpreter instance.
192    pub fn ui_json(&self) -> Result<String, JsValue> {
193        super::encode(
194            &self
195                .inner
196                .ui_document()
197                .map_err(|value| JsValue::from_str(&value.to_string()))?,
198        )
199    }
200
201    /// Validates a semantic input and invokes its Rust-owned callback transactionally.
202    pub fn invoke_ui(&mut self, event_json: &str) -> Result<String, JsValue> {
203        if event_json.len() > 8192 {
204            return Err(JsValue::from_str("UI event exceeds 8 KiB"));
205        }
206        let event = serde_json::from_str(event_json).map_err(super::json_error)?;
207        super::encode(
208            &self
209                .inner
210                .invoke_ui(&event)
211                .map_err(|value| error(&value))?,
212        )
213    }
214    /// Constructs and initializes a program. Imports come only from `modules_json`.
215    #[wasm_bindgen(constructor)]
216    pub fn new(source: &str, modules_json: &str) -> Result<Self, JsValue> {
217        Ok(Self {
218            inner: SpatialRuntime::new(source, &modules(modules_json)?).map_err(|e| error(&e))?,
219        })
220    }
221    /// Advances a bounded number of fixed simulation ticks.
222    pub fn step(&mut self, count: f64) -> Result<String, JsValue> {
223        if !count.is_finite() || count.fract() != 0.0 || !(0.0..=600.0).contains(&count) {
224            return Err(JsValue::from_str(
225                "step count must be an integer between 0 and 600",
226            ));
227        }
228        super::encode(&self.inner.step(count as u32).map_err(|e| error(&e))?)
229    }
230    /// Runs pending system `done` callbacks once. A failure preserves the run.
231    /// Use `free()` separately to release the WebAssembly allocation.
232    pub fn finish(&mut self) -> Result<String, JsValue> {
233        super::encode(&self.inner.finish().map_err(|e| error(&e))?)
234    }
235    /// Reads serializable component values, logs and source-linked execution trace.
236    pub fn snapshot_json(&self) -> Result<String, JsValue> {
237        super::encode(&self.inner.snapshot())
238    }
239    /// Returns an algebraic JSON result for read-only tensor inspection.
240    ///
241    /// `target_json` encodes a language [`InspectionTarget`], limited to 4 KiB.
242    /// The result is `{"Ok": TensorInspection}` or `{"Err": InspectionError}`.
243    /// It never evaluates source, dispatches callbacks, or alters accepted state.
244    /// The SDK bounds sampling and preserves exact numeric previews and extrema.
245    pub fn inspect_tensor_json(&self, target_json: &str) -> String {
246        let result = if target_json.len() > 4096 {
247            Err(InspectionError::InvalidTarget)
248        } else {
249            serde_json::from_str::<InspectionTarget>(target_json)
250                .map_err(|_| InspectionError::InvalidTarget)
251                .and_then(|target| self.inner.snapshot().tensor_inspection(&target))
252        };
253        serde_json::to_string(&result).unwrap_or_else(|_| "{\"Err\":\"encoding\"}".into())
254    }
255    /// Exports one global tensor as an owned, packed little-endian byte array.
256    ///
257    /// Strided views are materialized in logical row-major order. The returned
258    /// Uint8Array owns its bytes, so later stepping, allocation or `free()` cannot
259    /// invalidate it. Read [`Self::tensor_metadata_json`] for the element format.
260    pub fn tensor_bytes(&self, name: &str) -> Result<Vec<u8>, JsValue> {
261        let snapshot = self.inner.snapshot();
262        let Some(Value::Tensor(tensor)) = snapshot.globals.get(name) else {
263            return Err(JsValue::from_str("global must name a Tensor"));
264        };
265        Ok(tensor.to_le_bytes())
266    }
267    /// Describes the independent packed export, rather than its internal view strides.
268    pub fn tensor_metadata_json(&self, name: &str) -> Result<String, JsValue> {
269        let snapshot = self.inner.snapshot();
270        let Some(Value::Tensor(tensor)) = snapshot.globals.get(name) else {
271            return Err(JsValue::from_str("global must name a Tensor"));
272        };
273        super::encode(&serde_json::json!({
274            "dtype": tensor.dtype(), "shape": tensor.shape(),
275            "byte_length": tensor.byte_len(), "byte_order": "little", "order": "row-major",
276        }))
277    }
278    /// Generates validated render data. Browser code owns GPU and DOM resources.
279    pub fn render_json(&self) -> Result<String, JsValue> {
280        super::encode(&self.inner.render().map_err(|e| error(&e))?)
281    }
282    /// Dispatches a component method such as `click` without browser-owned behavior.
283    pub fn invoke_entity(
284        &mut self,
285        entity: f64,
286        method: &str,
287        args_json: &str,
288    ) -> Result<String, JsValue> {
289        if args_json.len() > 64 * 1024 {
290            return Err(JsValue::from_str("arguments exceed 64 KiB"));
291        }
292        if !entity.is_finite()
293            || entity.fract() != 0.0
294            || !(1.0..=9_007_199_254_740_991.0).contains(&entity)
295        {
296            return Err(JsValue::from_str(
297                "entity ID must be a positive exact integer",
298            ));
299        }
300        let args: Vec<Value> = serde_json::from_str(args_json).map_err(super::json_error)?;
301        super::encode(
302            &self
303                .inner
304                .invoke_entity(entity as u64, method, &args)
305                .map_err(|e| error(&e))?,
306        )
307    }
308    /// Invokes the typed callback held in a Rust-owned component field.
309    /// The browser supplies only entity, component and field identity.
310    pub fn invoke_component(
311        &mut self,
312        entity: f64,
313        component: &str,
314        field: &str,
315        args_json: &str,
316    ) -> Result<String, JsValue> {
317        if args_json.len() > 64 * 1024 {
318            return Err(JsValue::from_str("arguments exceed 64 KiB"));
319        }
320        if !entity.is_finite()
321            || entity.fract() != 0.0
322            || !(1.0..=9_007_199_254_740_991.0).contains(&entity)
323        {
324            return Err(JsValue::from_str(
325                "entity ID must be a positive exact integer",
326            ));
327        }
328        let args: Vec<Value> = serde_json::from_str(args_json).map_err(super::json_error)?;
329        super::encode(
330            &self
331                .inner
332                .invoke_component(entity as u64, component, field, &args)
333                .map_err(|e| error(&e))?,
334        )
335    }
336    /// Calls a language action with typed JSON arguments. Invalid calls preserve state.
337    pub fn invoke(&mut self, name: &str, args_json: &str) -> Result<String, JsValue> {
338        if args_json.len() > 64 * 1024 {
339            return Err(JsValue::from_str("arguments exceed 64 KiB"));
340        }
341        let args: Vec<Value> = serde_json::from_str(args_json).map_err(super::json_error)?;
342        super::encode(&self.inner.invoke(name, &args).map_err(|e| error(&e))?)
343    }
344}