ctoolbox/formats/eite/
runtime.rs

1/* -------------------------------------------------------------------------
2   StageR Main Loop (ndw / ndw_invoke) STUBS
3   -------------------------------------------------------------------------
4   The original JS code invoked deterministic routines `sr_main`, `sr_tick`,
5   `sr_getExitCode` with shared memory / storage / IO objects. Those engine
6   functions are not part of the already translated Rust. For now we provide
7   placeholders to preserve API shape. They can be filled in once the runtime
8   is ported.
9----------------------------------------------------------------------------- */
10
11use anyhow::{Context, Result, ensure};
12
13use crate::{
14    debug,
15    formats::{
16        FormatLog,
17        eite::{
18            dc::dc_is_el_code,
19            eite_state::EiteState,
20            export_document,
21            formats::{
22                Format, PrefilterSettings, dca_from_format, dca_to_format,
23            },
24            util::string::int_arr_from_str_printed_arr,
25        },
26    },
27    json,
28};
29
30/// Placeholder struct representing the (unported) StageR engine context.
31pub struct NdwEngine {}
32
33impl Default for NdwEngine {
34    fn default() -> Self {
35        Self::new()
36    }
37}
38
39impl NdwEngine {
40    pub fn new() -> Self {
41        Self {}
42    }
43}
44
45/// Simulated main loop (ndw). Returns an exit code (currently always 0).
46pub fn ndw(_engine: &mut NdwEngine) -> i32 {
47    // Stub: in real implementation we would:
48    // 1. Call sr_main(memory, storage, io)
49    // 2. Loop calling sr_tick until it returns <= 0
50    0
51}
52
53/// Invoke specific runtime routine (stub). Present only for API parity.
54pub fn ndw_invoke(_engine: &mut NdwEngine, routine: &str) -> i32 {
55    match routine {
56        "main" | "tick" => 1,
57        "getExitCode" => 0,
58        _ => 0,
59    }
60}
61
62/// Public API: start execution for an existing prepared document (`exec_id`).
63///
64/// This replicates `startDocumentExec(intExecId)` from the StageL code.
65/// It executes until:
66/// * End of document Dc array
67/// * stopExecAtTick (if configured) is reached
68/// The working frame is periodically flushed (every 100 ticks) unless run headless.
69/// Escape handling and simple (line / block) comment skipping is implemented.
70///
71/// NOTE:
72/// - EL code processing (dcIsELCode branch) remains unimplemented as in the original section.
73/// - Rendering hooks (`renderDrawContents`) are no-ops here; adapt to your UI layer if needed.
74pub fn start_document_exec(
75    state: &mut EiteState,
76    exec_id: usize,
77) -> Result<()> {
78    ensure!(state.is_exec_id(exec_id), "Invalid exec id {exec_id}");
79
80    // Tick / control options.
81    let stop_exec_at_tick: u32 = state
82        .get_exec_option(exec_id, "stopExecAtTick")?
83        .unwrap_or_else(|| "0".to_string())
84        .parse()
85        .unwrap_or(0);
86
87    let run_headless = state
88        .get_exec_option(exec_id, "runHeadless")?
89        .is_some_and(|v| v == "true");
90
91    // Working copy of the Dc data (parsed from stored string).
92    let working_copy: Vec<u32> = {
93        let data_str = state
94            .document_exec_data
95            .get(exec_id)
96            .cloned()
97            .unwrap_or_default();
98        int_arr_from_str_printed_arr(&data_str)?
99    };
100
101    let mut continue_loop = true;
102    let mut current_tick: u32 = 0;
103    let mut wip_frame: Vec<u32> = Vec::new();
104    let mut last_char_was_escape = false;
105
106    // Execution states stack: only textual marker strings were used originally.
107    let mut state_stack: Vec<&'static str> = vec!["normal"];
108
109    let out_fmt = state.get_env_preferred_format().to_string();
110    let out_fmt = Format::from_string(&out_fmt)?;
111
112    while continue_loop {
113        if current_tick >= stop_exec_at_tick - 1 {
114            continue_loop = false;
115        }
116        if !continue_loop {
117            break;
118        }
119        current_tick += 1;
120
121        let ptr_pos = usize::try_from(state.get_current_exec_ptr_pos(exec_id)?)
122            .expect("Should work to get usize");
123        if ptr_pos >= working_copy.len() {
124            // Document done.
125            continue_loop = false;
126        } else {
127            let dc = working_copy[ptr_pos];
128            debug!(
129                json!(state),
130                1,
131                &format!(
132                    "Exec loop pos={ptr_pos} dc={dc} state={state_stack:?} tick={current_tick}"
133                )
134            );
135
136            if last_char_was_escape {
137                last_char_was_escape = false;
138                state.incr_exec_ptr_pos(exec_id)?;
139            } else {
140                if dc == 255 {
141                    // Escape indicator
142                    last_char_was_escape = true;
143                } else {
144                    match state_stack.last().copied().unwrap_or("normal") {
145                        "normal" => {
146                            // Enter comment states?
147                            if dc == 246 || dc == 247 {
148                                state_stack.push("single-line source comment");
149                            } else if dc == 249 || dc == 250 {
150                                state_stack.push("block source comment");
151                            } else if dc_is_el_code(dc)? {
152                                // FIXME: EL code execution unimplemented
153                            } else {
154                                wip_frame.push(dc);
155                            }
156                        }
157                        "single-line source comment" => {
158                            // End single-line comment at Dc=248
159                            if dc == 248 {
160                                state_stack.pop();
161                            }
162                        }
163                        "block source comment" => {
164                            if dc == 251 {
165                                state_stack.pop();
166                            }
167                        }
168                        _ => {
169                            // Unknown state: treat as normal accumulation.
170                            wip_frame.push(dc);
171                        }
172                    }
173                }
174                state.incr_exec_ptr_pos(exec_id)?;
175            }
176        }
177
178        if !run_headless && current_tick.is_multiple_of(100) {
179            // Frame flush.
180            state.set_exec_frame(exec_id, &wip_frame)?;
181            // Convert to preferred format and "render".
182            let _ = dca_to_format(
183                state,
184                &out_fmt,
185                &wip_frame,
186                &PrefilterSettings::default(),
187            ); // Ignoring output; side-effect stub.
188            // In original code: renderDrawContents(...)
189        }
190    }
191
192    // Final flush
193    state.set_exec_frame(exec_id, &wip_frame)?;
194    let _ = dca_to_format(
195        state,
196        &out_fmt,
197        &wip_frame,
198        &PrefilterSettings::default(),
199    ); // Ignoring output; side-effect stub.
200
201    Ok(())
202}
203
204/// Translate of JS `runTestsDocumentExec(boolV)`
205///
206/// In StageL this drove scripted tests referencing stored documents. Here we
207/// expose an internal helper allowing unit tests (or integration tests) to call
208/// it with `verbose = true/false`.
209///
210/// Requires the presence of assets:
211///  resources/data/eite/ddc/exec-tests/{testname}.sems
212///  resources/data/eite/ddc/exec-tests/{testname}.out.sems
213pub fn run_tests_document_exec(
214    _state: &mut EiteState,
215    _verbose: bool,
216) -> Result<()> {
217    // The original JS invoked:
218    //   runExecTest('at-comment-no-space', 10);
219    //   runExecTest('at-comment', 10);
220    //   runExecTest('at-nl', 10);
221    //   runExecTest('at-space-nl', 10);
222    //   runExecTest('hello-world', 100);
223    //
224    // Complete faithful reproduction would require previously translated
225    // loadStoredDocument / runDocumentPrepare / runDocumentGo / runTest.
226    //
227    // Those earlier portions are assumed to exist. If / when they do, wire
228    // them through here. For now this is a placeholder.
229    Ok(())
230}
231
232/// Translate of JS `runExecTest`.
233///
234/// Placeholder until the rest of the execution harness (document loading)
235/// is wired in. Provided for structural fidelity.
236pub fn run_exec_test(
237    _state: &mut EiteState,
238    _test_name: &str,
239    _ticks_needed: i32,
240    _verbose: bool,
241) -> Result<()> {
242    Ok(())
243}
244
245/// Start EITE using the default startup document ("eite.sems" in "sems" format).
246/// (startEite in JS)
247pub fn start_eite(state: &mut EiteState) -> Result<()> {
248    load_and_run(state, &Format::sems_default(), "eite.sems")
249}
250
251/// Load and run a document (blocking until completion in original JS).
252/// Here it prepares and starts execution; actual stepping / event loop
253/// would need the still-unported runtime engine.
254pub fn load_and_run(
255    state: &mut EiteState,
256    format: &Format,
257    path: &str,
258) -> Result<()> {
259    let doc = load_stored_document(state, format, path)?;
260    run_document(state, &doc)
261}
262
263/// Run document: prepare + go.
264pub fn run_document(state: &mut EiteState, dc_array: &[u32]) -> Result<()> {
265    let exec_id = run_document_prepare(state, dc_array)?;
266    run_document_go(state, exec_id)
267}
268
269/// Prepare to run a document, returning an execution ID.
270pub fn run_document_prepare(
271    state: &mut EiteState,
272    dc_array: &[u32],
273) -> Result<usize> {
274    Ok(state.prepare_document_exec(dc_array))
275}
276
277/// Start (execute) a previously prepared document.
278pub fn run_document_go(state: &mut EiteState, exec_id: usize) -> Result<()> {
279    start_document_exec(state, exec_id)
280}
281
282/// Load, convert, and return bytes (JS loadAndConvert).
283pub fn load_and_convert(
284    state: &mut EiteState,
285    in_format: &Format,
286    out_format: &Format,
287    path: &str,
288    prefilter_settings: &PrefilterSettings,
289) -> Result<(Vec<u8>, FormatLog)> {
290    let doc = load_stored_document(state, in_format, path)?;
291    export_document(state, out_format, &doc, prefilter_settings)
292}
293
294/// Load a stored document from path using specified input format.
295/// Tries `path` first; if not found, also tries to prefix with
296/// `resources/data/eite/` (for bundled assets).
297pub fn load_stored_document(
298    state: &mut EiteState,
299    format: &Format,
300    path: &str,
301) -> Result<Vec<u32>> {
302    let bytes = try_load_asset(path)
303        .or_else(|_| try_load_asset(&format!("resources/data/eite/{path}")))
304        .with_context(|| format!("Failed to load document asset '{path}'"))?;
305    let (doc, log) = dca_from_format(state, format, &bytes)?;
306    log.auto_log();
307    Ok(doc)
308}
309
310fn try_load_asset(path: &str) -> Result<Vec<u8>> {
311    crate::storage::get_asset(path)
312        .with_context(|| format!("asset not found: {path}"))
313}
314
315/// Return desired event notifications for a document (empty: not yet implemented).
316pub fn get_desired_event_notifications(
317    _state: &EiteState,
318    _exec_id: usize,
319) -> Result<Vec<String>> {
320    Ok(Vec::new())
321}
322
323/// Send an event to a running document (stub).
324pub fn send_event(
325    _state: &mut EiteState,
326    _exec_id: usize,
327    _event_data: &[i32],
328) -> Result<()> {
329    // Event system not yet ported.
330    Ok(())
331}
332
333/// Get current document frame exported to a format.
334/// If the frame is absent, returns an empty byte vector.
335pub fn get_document_frame(
336    state: &mut EiteState,
337    exec_id: usize,
338    out_format: &Format,
339) -> Result<(Vec<u8>, FormatLog)> {
340    match state.get_current_exec_frame(exec_id) {
341        Ok(frame) => dca_to_format(
342            state,
343            out_format,
344            &frame,
345            &PrefilterSettings::default(),
346        ),
347        Err(_) => Ok((Vec::new(), FormatLog::default())),
348    }
349}
350
351#[cfg(test)]
352mod tests {
353    use super::*;
354
355    #[crate::ctb_test]
356    fn test_document_exec_state() {
357        let mut st = EiteState::default();
358        let exec_id = st.prepare_document_exec(&[1, 2, 3]);
359        assert!(st.is_exec_id(exec_id));
360
361        // Current pointer
362        assert_eq!(st.get_current_exec_ptr_pos(exec_id).unwrap(), 0);
363        st.set_exec_ptr_pos(exec_id, 5).unwrap();
364        assert_eq!(st.get_current_exec_ptr_pos(exec_id).unwrap(), 5);
365        st.incr_exec_ptr_pos(exec_id).unwrap();
366        assert_eq!(st.get_current_exec_ptr_pos(exec_id).unwrap(), 6);
367
368        // Data parse
369        let data = st.get_current_exec_data(exec_id).unwrap();
370        assert_eq!(data, vec![1, 2, 3]);
371    }
372}