tawasuyu

tawasuyu/llimphi

Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.

Rust 614Markdown 274TOML 149RON 310606 Definiciones

⎇ 168cd187519c0ad575d3ce63baee2f164c38d9d1 llimphi-wasm-runner/src/lib.rs Rust · 558 líneas · 21.6 KB Historia Quién tocó qué crudo

chore: refresco desde el monorepo — vello 0.9 / wgpu 29, 120 miembros d251eb32b · Sergio ·

1//! llimphi-wasm-runner — lado host de las apps WASM Tier 3.
2//!
3//! [`WasmGuest`] carga un `.wasm` que sigue el ABI de `llimphi-wasm-app-sdk`,
4//! lo instancia con wasmi y corre su bucle Elm:
5//!
6//! 1. `wasm_init` construye el `Model` del guest (vive en la instancia).
7//! 2. `wasm_view` devuelve un [`WireNode`] serializado; lo deserializamos y lo
8//!    cacheamos.
9//! 3. [`wire_to_view`] lo materializa en un `View<RunnerMsg>` Llimphi real.
10//! 4. Un click emite `RunnerMsg::Guest(bytes)`; [`WasmGuest::dispatch`] rebota
11//!    esos bytes a `wasm_dispatch` y refresca la vista.
12//!
13//! El host mantiene un único `Store` vivo por app, así el `Model` persiste
14//! entre frames — exactamente el contrato Elm de Llimphi, sólo que la
15//! transición corre del otro lado de la frontera WASM.
16//!
17//! Es el gemelo, del lado Linux, del ejecutor de apps WASM de wawa
18//! (`sys_render_frame` + caps por frontera física): el mismo espíritu de
19//! "abrir apps servidas/distribuidas", pero pintando con Llimphi en vez de un
20//! framebuffer crudo.
21
22use llimphi_ui::llimphi_layout::taffy::{
23    prelude::{length, percent, Dimension, FlexDirection, Rect, Size, Style},
24    AlignItems, JustifyContent,
25};
26use llimphi_ui::llimphi_raster::peniko::Color;
27use llimphi_ui::{Key, KeyEvent, KeyState, NamedKey, View};
28use llimphi_wire_view::{Align, Dim, Dir, Justify, TextAlign, WireInput, WireNode};
29use kikin_wasmi::{Concesion, Reja, Requiere};
30use wasmi::{Caller, CompilationMode, Config, Engine, Linker, Memory, Module, Store, TypedFunc};
31
32pub use format::Permisos;
33pub use llimphi_wire_view::{EventId, EventPayload};
34
35/// Mensaje del host. Un evento lleva el `EventId` del control y su `EventPayload`
36/// (Click/Text/Toggle); el host los rebota a `wasm_dispatch` y el guest
37/// reconstruye su `Msg`. `Focus` cambia el input enfocado (no cruza la frontera).
38#[derive(Clone, Debug)]
39pub enum RunnerMsg {
40    /// Evento de un control: id del handler + payload.
41    Event(EventId, EventPayload),
42    /// El foco de teclado pasó a este input (o a ninguno).
43    Focus(Option<EventId>),
44    /// Abrir/cerrar el dropdown con este `on_select` id (estado host-side).
45    ToggleSelect(EventId),
46}
47
48/// Una app WASM guest cargada y viva. Mantén una por app: el `Model` del guest
49/// reside en su instancia y persiste entre `dispatch`.
50pub struct WasmGuest {
51    store: Store<()>,
52    memory: Memory,
53    f_view: TypedFunc<(), u64>,
54    f_alloc: TypedFunc<u32, u32>,
55    f_dispatch: TypedFunc<(u32, u32, u32), ()>,
56    f_free: TypedFunc<(u32, u32), ()>,
57    /// Última vista decodificada — el host la materializa cada frame sin volver
58    /// a cruzar la frontera salvo tras un `dispatch`.
59    view: WireNode,
60    /// Input con el foco de teclado (su `on_input` EventId), si hay alguno.
61    focused: Option<EventId>,
62    /// Dropdown abierto (su `on_select` EventId), si hay alguno.
63    open_select: Option<EventId>,
64}
65
66impl WasmGuest {
67    /// Instancia el `.wasm` y corre `wasm_init` + el primer `wasm_view`.
68    ///
69    /// `permisos` es el bitfield efectivo (típicamente
70    /// `format::permisos_efectivos(declarados, concedidos)` que calcula
71    /// `llimphi-wasm-dist` tras verificar la concesión Ed25519). Gatea qué host
72    /// imports se enlazan: si el bit falta, la función no se registra y un guest
73    /// que la importe **trap-ea al instanciar** — frontera física, no tabla.
74    pub fn load(wasm_bytes: &[u8], permisos: Permisos) -> Result<Self, String> {
75        // Eager: los traps de compilación salen aquí, no en pleno frame. Igual
76        // criterio que llimphi-plugin-host y el kernel de wawa.
77        let mut config = Config::default();
78        config.compilation_mode(CompilationMode::Eager);
79        let engine = Engine::new(&config);
80        let module = Module::new(&engine, wasm_bytes).map_err(|e| format!("compilar wasm: {e}"))?;
81        let mut store = Store::new(&engine, ());
82        // Una app Tier 3 pura (sólo UI) no importa nada; las capacidades (red…)
83        // se enlazan aquí sólo si el permiso correspondiente está concedido.
84        let linker = build_linker(&engine, permisos)?;
85        let instance = linker
86            .instantiate_and_start(&mut store, &module)
87            .map_err(|e| format!("instanciar wasm: {e}"))?;
88
89        let memory = instance
90            .get_memory(&store, "memory")
91            .ok_or("el guest no exporta `memory`")?;
92        let f_init = instance
93            .get_typed_func::<(), ()>(&store, "wasm_init")
94            .map_err(|e| format!("export `wasm_init`: {e}"))?;
95        let f_view = instance
96            .get_typed_func::<(), u64>(&store, "wasm_view")
97            .map_err(|e| format!("export `wasm_view`: {e}"))?;
98        let f_alloc = instance
99            .get_typed_func::<u32, u32>(&store, "wasm_alloc")
100            .map_err(|e| format!("export `wasm_alloc`: {e}"))?;
101        let f_dispatch = instance
102            .get_typed_func::<(u32, u32, u32), ()>(&store, "wasm_dispatch")
103            .map_err(|e| format!("export `wasm_dispatch`: {e}"))?;
104        let f_free = instance
105            .get_typed_func::<(u32, u32), ()>(&store, "wasm_free")
106            .map_err(|e| format!("export `wasm_free`: {e}"))?;
107
108        f_init
109            .call(&mut store, ())
110            .map_err(|e| format!("wasm_init trap: {e}"))?;
111
112        let mut guest = WasmGuest {
113            store,
114            memory,
115            f_view,
116            f_alloc,
117            f_dispatch,
118            f_free,
119            view: WireNode::default(),
120            focused: None,
121            open_select: None,
122        };
123        guest.refresh()?;
124        Ok(guest)
125    }
126
127    /// La última vista decodificada del guest.
128    pub fn view(&self) -> &WireNode {
129        &self.view
130    }
131
132    /// Vuelve a pedir `wasm_view` y decodifica el `WireNode`.
133    fn refresh(&mut self) -> Result<(), String> {
134        let packed = self
135            .f_view
136            .call(&mut self.store, ())
137            .map_err(|e| format!("wasm_view trap: {e}"))?;
138        let ptr = (packed >> 32) as usize;
139        let len = (packed & 0xffff_ffff) as usize;
140        let data = self.memory.data(&self.store);
141        let end = ptr.checked_add(len).ok_or("wasm_view: rango desbordado")?;
142        if end > data.len() {
143            return Err("wasm_view: rango fuera de la memoria del guest".into());
144        }
145        let bytes = data[ptr..end].to_vec();
146        // Soltamos el buffer del guest antes de propagar errores de decodificación.
147        let _ = self.f_free.call(&mut self.store, (ptr as u32, len as u32));
148        self.view = postcard::from_bytes(&bytes).map_err(|e| format!("decodificar WireNode: {e}"))?;
149        Ok(())
150    }
151
152    /// Rebota un evento al guest: serializa el `payload`, lo escribe y corre
153    /// `wasm_dispatch(event_id, ptr, len)`, luego refresca la vista.
154    pub fn dispatch(&mut self, event_id: EventId, payload: EventPayload) -> Result<(), String> {
155        let bytes = postcard::to_allocvec(&payload).map_err(|e| format!("encode payload: {e}"))?;
156        let len = bytes.len() as u32;
157        let ptr = self
158            .f_alloc
159            .call(&mut self.store, len)
160            .map_err(|e| format!("wasm_alloc trap: {e}"))?;
161        self.memory
162            .write(&mut self.store, ptr as usize, &bytes)
163            .map_err(|e| format!("escribir payload: {e}"))?;
164        self.f_dispatch
165            .call(&mut self.store, (event_id, ptr, len))
166            .map_err(|e| format!("wasm_dispatch trap: {e}"))?;
167        self.refresh()
168    }
169
170    /// Aplica un `RunnerMsg`. Conveniencia para el `update` del host.
171    pub fn apply(&mut self, msg: &RunnerMsg) -> Result<(), String> {
172        match msg {
173            RunnerMsg::Event(id, payload) => {
174                // Cualquier evento cierra un dropdown abierto (elegir/clicar = cerrar).
175                self.open_select = None;
176                self.dispatch(*id, payload.clone())
177            }
178            RunnerMsg::Focus(f) => {
179                self.focused = *f;
180                Ok(())
181            }
182            RunnerMsg::ToggleSelect(id) => {
183                self.open_select = if self.open_select == Some(*id) {
184                    None
185                } else {
186                    Some(*id)
187                };
188                Ok(())
189            }
190        }
191    }
192
193    /// El input enfocado actualmente (su `on_input` EventId).
194    pub fn focused(&self) -> Option<EventId> {
195        self.focused
196    }
197
198    /// Traduce un evento de teclado a un `RunnerMsg` para el input enfocado:
199    /// computa "valor actual + tecla → texto nuevo" (Backspace borra el último
200    /// carácter; un carácter se anexa) y emite `Text`. Devuelve `None` si no hay
201    /// foco o la tecla no edita. Modelo value-driven: el guest es la fuente de
202    /// verdad del texto; el host no guarda buffer (cursor siempre al final, MVP).
203    pub fn key_to_msg(&self, event: &KeyEvent) -> Option<RunnerMsg> {
204        if event.state != KeyState::Pressed {
205            return None;
206        }
207        let id = self.focused?;
208        let inp = find_input(&self.view, id)?;
209        let nuevo = edit_value(&inp.value, &event.key, event.text.as_deref(), inp.multiline)?;
210        Some(RunnerMsg::Event(id, EventPayload::Text(nuevo)))
211    }
212
213    /// `RunnerMsg` para fijar el foco (lo usa el `on_focus` del host).
214    pub fn focus_msg(id: Option<u64>) -> RunnerMsg {
215        RunnerMsg::Focus(id.map(|x| x as EventId))
216    }
217
218    /// Materializa la vista cacheada en un `View<RunnerMsg>` Llimphi real.
219    pub fn render(&self) -> View<RunnerMsg> {
220        wire_to_view(&self.view, self.focused, self.open_select)
221    }
222}
223
224/// Computa el texto nuevo de un campo dado el actual y una tecla: Backspace borra
225/// el último carácter; un carácter se anexa; Enter inserta `\n` sólo si el campo
226/// es `multiline` (en uno de una línea no edita); otras teclas no editan
227/// (`None`). Pura — el corazón del modelo value-driven, testeable sin `KeyEvent`.
228pub fn edit_value(actual: &str, key: &Key, text: Option<&str>, multiline: bool) -> Option<String> {
229    match key {
230        Key::Named(NamedKey::Backspace) => {
231            let mut s = actual.to_string();
232            s.pop();
233            Some(s)
234        }
235        Key::Named(NamedKey::Enter) if multiline => Some(format!("{actual}\n")),
236        Key::Character(_) => text.map(|t| format!("{actual}{t}")),
237        _ => None,
238    }
239}
240
241/// Busca el `WireInput` cuyo `on_input` es `id` en el árbol.
242fn find_input(node: &WireNode, id: EventId) -> Option<&WireInput> {
243    if node.on_input == Some(id) {
244        if let Some(inp) = &node.input {
245            return Some(inp);
246        }
247    }
248    node.children.iter().find_map(|c| find_input(c, id))
249}
250
251// Colores por defecto de los controles (el estilo de la caja viene del nodo).
252const INPUT_TEXT: [u8; 4] = [230, 235, 245, 255];
253const INPUT_PLACEHOLDER: [u8; 4] = [120, 130, 145, 255];
254const INPUT_BORDER: [u8; 4] = [80, 90, 110, 255];
255const INPUT_BORDER_FOCUS: [u8; 4] = [90, 160, 230, 255];
256const SLIDER_TRACK: [u8; 4] = [40, 46, 58, 255];
257const SLIDER_FILL: [u8; 4] = [90, 160, 230, 255];
258const SELECT_BG: [u8; 4] = [28, 34, 44, 255];
259const SELECT_ITEM: [u8; 4] = [34, 40, 52, 255];
260const SELECT_SEL: [u8; 4] = [50, 70, 100, 255];
261
262/// Materializa un [`WireNode`] en un `View<RunnerMsg>` Llimphi real,
263/// recursivamente. `focused` = input con el foco; `open_select` = dropdown
264/// abierto (para pintar su lista).
265pub fn wire_to_view(
266    node: &WireNode,
267    focused: Option<EventId>,
268    open_select: Option<EventId>,
269) -> View<RunnerMsg> {
270    let mut style = Style {
271        flex_direction: match node.dir {
272            Dir::Row => FlexDirection::Row,
273            Dir::Column | Dir::Block => FlexDirection::Column,
274        },
275        size: Size {
276            width: dim(node.width),
277            height: dim(node.height),
278        },
279        flex_grow: node.grow,
280        ..Default::default()
281    };
282    if node.gap != 0.0 {
283        style.gap = Size {
284            width: length(node.gap),
285            height: length(node.gap),
286        };
287    }
288    let [pt, pr, pb, pl] = node.padding;
289    style.padding = Rect {
290        top: length(pt),
291        right: length(pr),
292        bottom: length(pb),
293        left: length(pl),
294    };
295    if let Some(a) = node.align {
296        style.align_items = Some(map_align(a));
297    }
298    if let Some(j) = node.justify {
299        style.justify_content = Some(map_justify(j));
300    }
301
302    let mut view = View::new(style);
303    if let Some(fill) = node.fill {
304        view = view.fill(color(fill));
305    }
306    if node.radius != 0.0 {
307        view = view.radius(node.radius as f64);
308    }
309
310    if let Some(inp) = &node.input {
311        // Campo editable: caja focusable que muestra value/placeholder + caret.
312        let id = node.on_input.unwrap_or(0);
313        let is_focused = focused == Some(id);
314        let (shown, col) = display_input(inp, is_focused);
315        let border = if is_focused { INPUT_BORDER_FOCUS } else { INPUT_BORDER };
316        view = view
317            .text(shown, 20.0, color(col))
318            .border(1.5, color(border))
319            .focusable(id as u64)
320            .on_click(RunnerMsg::Focus(Some(id)));
321    } else if let Some(checked) = node.toggle {
322        // Checkbox: glifo clickable que alterna su estado.
323        let id = node.on_toggle.unwrap_or(0);
324        let glyph = if checked { "\u{2611}" } else { "\u{2610}" }; // ☑ / ☐
325        view = view
326            .text(glyph, 24.0, color(INPUT_TEXT))
327            .on_click(RunnerMsg::Event(id, EventPayload::Toggle(!checked)));
328    } else if let Some(sl) = &node.slider {
329        // Slider: track con una barra de relleno proporcional; click-en-x fija
330        // el valor. id/min/max van Copy a la clausura.
331        let id = node.on_value.unwrap_or(0);
332        let (min, max) = (sl.min, sl.max);
333        let frac = if max > min {
334            ((sl.value - min) / (max - min)).clamp(0.0, 1.0)
335        } else {
336            0.0
337        };
338        view = view
339            .fill(color(SLIDER_TRACK))
340            .radius(6.0)
341            .on_click_at(move |lx, _ly, w, _h| {
342                let f = if w > 0.0 { (lx / w).clamp(0.0, 1.0) } else { 0.0 };
343                Some(RunnerMsg::Event(id, EventPayload::Value(min + f * (max - min))))
344            });
345        let bar = View::new(Style {
346            size: Size {
347                width: percent(frac),
348                height: percent(1.0_f32),
349            },
350            ..Default::default()
351        })
352        .fill(color(SLIDER_FILL))
353        .radius(6.0);
354        view = view.children(vec![bar]);
355    } else if let Some(sel) = &node.select {
356        // Dropdown: header con la opción actual + lista inline cuando está abierto.
357        let id = node.on_select.unwrap_or(0);
358        let is_open = open_select == Some(id);
359        let current = sel
360            .options
361            .get(sel.selected as usize)
362            .cloned()
363            .unwrap_or_else(|| "\u{2014}".into()); // —
364        let arrow = if is_open { "\u{25b4}" } else { "\u{25be}" }; // ▴ / ▾
365        let header = item_box(36.0)
366            .fill(color(SELECT_BG))
367            .border(1.0, color(INPUT_BORDER))
368            .text(format!("{current}   {arrow}"), 18.0, color(INPUT_TEXT))
369            .on_click(RunnerMsg::ToggleSelect(id));
370        let mut kids = vec![header];
371        if is_open {
372            for (i, opt) in sel.options.iter().enumerate() {
373                let oid = i as u32;
374                let bg = if oid == sel.selected { SELECT_SEL } else { SELECT_ITEM };
375                kids.push(
376                    item_box(32.0)
377                        .fill(color(bg))
378                        .text(opt.clone(), 18.0, color(INPUT_TEXT))
379                        .on_click(RunnerMsg::Event(id, EventPayload::Select(oid))),
380                );
381            }
382        }
383        view = view.children(kids);
384    } else if let Some(rad) = &node.radio {
385        // Grupo de radio: todas las opciones visibles, la marcada con ◉. Cada
386        // fila emite Select(idx) (mismo payload que el dropdown).
387        let id = node.on_radio.unwrap_or(0);
388        let kids: Vec<View<RunnerMsg>> = rad
389            .options
390            .iter()
391            .enumerate()
392            .map(|(i, opt)| {
393                let oid = i as u32;
394                let marcado = oid == rad.selected;
395                let glyph = if marcado { "\u{25c9}" } else { "\u{25cb}" }; // ◉ / ○
396                let col = if marcado { SLIDER_FILL } else { INPUT_TEXT };
397                item_box(30.0)
398                    .text(format!("{glyph}  {opt}"), 18.0, color(col))
399                    .on_click(RunnerMsg::Event(id, EventPayload::Select(oid)))
400            })
401            .collect();
402        view = view.children(kids);
403    } else {
404        if let Some(t) = &node.text {
405            view = view.text_aligned(
406                t.content.clone(),
407                t.size,
408                color(t.color),
409                map_text_align(t.align),
410            );
411        }
412        if let Some(id) = node.on_click {
413            view = view.on_click(RunnerMsg::Event(id, EventPayload::Click));
414        }
415    }
416
417    if !node.children.is_empty() {
418        view = view.children(
419            node.children
420                .iter()
421                .map(|c| wire_to_view(c, focused, open_select))
422                .collect(),
423        );
424    }
425    view
426}
427
428/// Una caja de fila a `height` px, ancho completo, contenido centrado a la
429/// izquierda con padding — el ladrillo del header/items del dropdown.
430fn item_box(height: f32) -> View<RunnerMsg> {
431    View::new(Style {
432        size: Size {
433            width: percent(1.0_f32),
434            height: length(height),
435        },
436        align_items: Some(AlignItems::CENTER),
437        padding: Rect {
438            left: length(10.0_f32),
439            right: length(10.0_f32),
440            top: length(0.0_f32),
441            bottom: length(0.0_f32),
442        },
443        ..Default::default()
444    })
445    .radius(6.0)
446}
447
448/// Texto a mostrar en un input y su color: value (o placeholder), enmascarado si
449/// es password, con un caret `│` cuando tiene el foco.
450fn display_input(inp: &WireInput, focused: bool) -> (String, [u8; 4]) {
451    let body = if inp.value.is_empty() {
452        if focused {
453            String::new()
454        } else {
455            return (inp.placeholder.clone(), INPUT_PLACEHOLDER);
456        }
457    } else if inp.password {
458        "\u{25cf}".repeat(inp.value.chars().count()) // ●
459    } else {
460        inp.value.clone()
461    };
462    let shown = if focused { format!("{body}\u{2502}") } else { body }; // │
463    (shown, INPUT_TEXT)
464}
465
466fn dim(d: Dim) -> Dimension {
467    match d {
468        Dim::Auto => Dimension::auto(),
469        Dim::Px(v) => length(v),
470        Dim::Pct(v) => percent(v),
471    }
472}
473
474fn color(c: [u8; 4]) -> Color {
475    Color::from_rgba8(c[0], c[1], c[2], c[3])
476}
477
478fn map_align(a: Align) -> AlignItems {
479    match a {
480        Align::Start => AlignItems::FLEX_START,
481        Align::Center => AlignItems::CENTER,
482        Align::End => AlignItems::FLEX_END,
483        Align::Stretch => AlignItems::STRETCH,
484    }
485}
486
487fn map_justify(j: Justify) -> JustifyContent {
488    match j {
489        Justify::Start => JustifyContent::FLEX_START,
490        Justify::Center => JustifyContent::CENTER,
491        Justify::End => JustifyContent::FLEX_END,
492        Justify::SpaceBetween => JustifyContent::SPACE_BETWEEN,
493        Justify::SpaceAround => JustifyContent::SPACE_AROUND,
494    }
495}
496
497fn map_text_align(a: TextAlign) -> llimphi_ui::llimphi_text::Alignment {
498    match a {
499        TextAlign::Start => llimphi_ui::llimphi_text::Alignment::Start,
500        TextAlign::Center => llimphi_ui::llimphi_text::Alignment::Center,
501        TextAlign::End => llimphi_ui::llimphi_text::Alignment::End,
502    }
503}
504
505// =====================================================================
506// Host imports — gateados por Permisos (frontera física, espejo host de
507// wawa::wasm::env). Namespace `"tawa"`. Si el bit no está, el import no se
508// enlaza y el guest que lo use trap-ea al instanciar.
509// =====================================================================
510
511/// Construye el linker para un guest con los `permisos` dados. Las funciones
512/// inocuas (log) van siempre; las que tocan recursos se enlazan sólo con su bit.
513///
514/// La DECISIÓN («¿este import se registra?») ya no vive aquí: la toma
515/// `kikin-core`, la misma política que gobierna al kernel de wawa y a los otros
516/// hosts de la suite. Aquí queda sólo lo que es nuestro: el ABI `tawa`.
517pub fn build_linker(engine: &Engine, permisos: Permisos) -> Result<Linker<()>, String> {
518    let mut reja = Reja::<(), Permisos>::nueva(engine, Concesion::nueva(permisos));
519
520    // `host_log` — traza, no toca recursos ajenos. Negarla no protegería nada.
521    reja.si("tawa::host_log", Requiere::Siempre, |l| {
522        l.func_wrap("tawa", "host_log", |caller: Caller<'_, ()>, ptr: i32, len: i32| {
523            if let Some(s) = read_utf8(&caller, ptr, len) {
524                eprintln!("[wasm] {s}");
525            }
526        })
527        .map(|_| ())
528        .map_err(|e| format!("enlazar host_log: {e}"))
529    })?;
530
531    // `host_net_request` — pide bytes a la red por hash (futuro: sobre
532    // BrahmanNet). Hoy es un stub que devuelve -1 (no implementado), pero la
533    // FRONTERA ya es real: sin `PERMISO_RED` el símbolo no existe y un guest
534    // que lo importe ni instancia.
535    reja.si("tawa::host_net_request", Requiere::Bit(format::PERMISO_RED), |l| {
536        l.func_wrap(
537            "tawa",
538            "host_net_request",
539            |_caller: Caller<'_, ()>, _ptr: i32, _len: i32| -> i32 { -1 },
540        )
541        .map(|_| ())
542        .map_err(|e| format!("enlazar host_net_request: {e}"))
543    })?;
544
545    Ok(reja.consumir())
546}
547
548fn read_utf8(caller: &Caller<'_, ()>, ptr: i32, len: i32) -> Option<String> {
549    let memory = caller.get_export("memory")?.into_memory()?;
550    let ptr = ptr.max(0) as usize;
551    let len = len.max(0) as usize;
552    let data = memory.data(caller);
553    let end = ptr.checked_add(len)?;
554    if end > data.len() {
555        return None;
556    }
557    String::from_utf8(data[ptr..end].to_vec()).ok()
558}