Skip to main content

qala_compiler/
wasm.rs

1//! the wasm-bindgen bridge: a `Qala` session struct that exposes the finished
2//! compile-and-VM pipeline to JavaScript.
3//!
4//! the module is two layers. the lower layer is plain Rust -- the `Qala`
5//! struct's state plus seven core methods that return typed serde structs
6//! directly. the native test suite drives this layer, since the core methods
7//! need no JavaScript runtime. the upper layer is the thin `#[wasm_bindgen]`
8//! impl block whose seven methods each call the matching core method and hand
9//! the result to `serde_wasm_bindgen::to_value`, returning a `JsValue`.
10//!
11//! the bridge never throws and never panics. a failed compile, a runtime
12//! error, and misuse such as `run` before `compile` are each structured data
13//! in the returned value carrying an `ok: false` or `status: "error"` flag --
14//! the playground branches on that flag and never needs a `try`/`catch`. a
15//! panic in WASM aborts the browser tab, so the no-panic discipline is the
16//! boundary contract; the panic hook installed in `new` is a bug-report
17//! safety net only.
18
19use wasm_bindgen::prelude::*;
20
21/// the result of `compile`. read by the playground's editor (the diagnostic
22/// underlines) and its bytecode panel (the disassembly).
23///
24/// the flat ok-flag shape rather than a serde-tagged enum: the playground
25/// branches on `ok` and reads a plain JavaScript object. derives
26/// `serde::Serialize` so the WASM bridge hands it straight to JS via
27/// `serde-wasm-bindgen`.
28#[derive(Debug, Clone, serde::Serialize)]
29pub struct CompileResult {
30    /// true on a clean compile (warnings are allowed), false on any error.
31    pub ok: bool,
32    /// the optimized bytecode disassembly; `Some` on success, `None` on error.
33    pub disassembly: Option<String>,
34    /// warnings on success, errors on failure -- `MonacoDiagnostic::severity`
35    /// distinguishes the two so the playground renders both from one array.
36    pub diagnostics: Vec<crate::diagnostics::MonacoDiagnostic>,
37}
38
39/// the result of `compile_arm64`. read by the playground's assembly view.
40///
41/// the same flat ok-flag shape as `CompileResult`: the playground branches
42/// on `ok`. derives `serde::Serialize` so the WASM bridge hands it straight
43/// to JS via `serde-wasm-bindgen`.
44#[derive(Debug, Clone, serde::Serialize)]
45pub struct Arm64Result {
46    /// true when the ARM64 backend produced assembly, false on any error
47    /// (a front-end error, or an unsupported-construct rejection).
48    pub ok: bool,
49    /// the AArch64 assembly text; `Some` on success, `None` on error.
50    pub assembly: Option<String>,
51    /// the diagnostics on failure -- an unsupported-construct rejection or a
52    /// front-end error -- empty on success.
53    pub diagnostics: Vec<crate::diagnostics::MonacoDiagnostic>,
54}
55
56/// the result of `run`. read by the playground's stack, variables, and
57/// console panels (the `state`) and its editor (the runtime-error underline).
58///
59/// derives `serde::Serialize` so the WASM bridge hands it straight to JS.
60#[derive(Debug, Clone, serde::Serialize)]
61pub struct RunResult {
62    /// true when the program ran to `Halt`, false on a runtime error or when
63    /// no program is compiled.
64    pub ok: bool,
65    /// the VM state at the stopping point -- on a runtime error this is the
66    /// fault-point snapshot, so the playground can show the stack and console
67    /// where execution stopped.
68    pub state: crate::vm::VmState,
69    /// the runtime-error diagnostic; `Some` on a fault, `None` otherwise.
70    pub error: Option<crate::diagnostics::MonacoDiagnostic>,
71}
72
73/// the result of `step`. read by the playground's step-through after each
74/// instruction.
75///
76/// derives `serde::Serialize` so the WASM bridge hands it straight to JS.
77#[derive(Debug, Clone, serde::Serialize)]
78pub struct StepResult {
79    /// `"ran"` when an ordinary instruction executed, `"halted"` when the VM
80    /// reached `Halt`, `"error"` on a runtime fault or when no program is
81    /// compiled. a plain string so the JavaScript side reads the discriminant
82    /// directly, without serde enum tagging.
83    pub status: String,
84    /// the VM state after the step (or the fault-point snapshot on an error).
85    pub state: crate::vm::VmState,
86    /// the runtime-error diagnostic; `Some` when `status` is `"error"`,
87    /// `None` otherwise.
88    pub error: Option<crate::diagnostics::MonacoDiagnostic>,
89}
90
91/// the result of one REPL evaluation. read by the playground's REPL console.
92///
93/// derives `serde::Serialize` so the WASM bridge hands it straight to JS.
94#[derive(Debug, Clone, serde::Serialize)]
95pub struct ReplResult {
96    /// true when the line compiled and evaluated, false on any error.
97    pub ok: bool,
98    /// the evaluated line's value, rendered and type-tagged; `Some` on
99    /// success, `None` on an error.
100    pub value: Option<crate::vm::StateValue>,
101    /// the persistent REPL console output, accumulated across every prior
102    /// REPL call.
103    pub console: Vec<String>,
104    /// the error diagnostic; `Some` on a failed line, `None` otherwise.
105    pub error: Option<crate::diagnostics::MonacoDiagnostic>,
106}
107
108/// the no-program-compiled `VmState`: a snapshot with every field zero or
109/// empty.
110///
111/// `VmState` has no public constructor and `Vm::get_state` needs a VM, so the
112/// misuse paths (run/step/get_state before a successful compile) build this
113/// literal directly -- every `VmState` field is `pub`. the playground can
114/// always read `.stack`, `.console`, and the rest off the returned value.
115fn empty_state() -> crate::vm::VmState {
116    crate::vm::VmState {
117        chunk_index: 0,
118        ip: 0,
119        current_line: 0,
120        stack: Vec::new(),
121        variables: Vec::new(),
122        console: Vec::new(),
123        leak_log: Vec::new(),
124    }
125}
126
127/// the diagnostic the misuse paths return when no program is compiled yet.
128///
129/// built as a struct literal -- every `MonacoDiagnostic` field is `pub`. the
130/// message is plain so the playground can show it verbatim; line and column
131/// are 1 (a harmless position, since this diagnostic underlines nothing) and
132/// `severity` is 1 (error).
133fn no_program_diagnostic() -> crate::diagnostics::MonacoDiagnostic {
134    crate::diagnostics::MonacoDiagnostic {
135        line: 1,
136        column: 1,
137        end_line: 1,
138        end_column: 1,
139        severity: 1,
140        message: "no program compiled -- call compile() first".to_string(),
141        category: None,
142    }
143}
144
145/// build the failed-compile `CompileResult` from a list of errors.
146///
147/// each `QalaError` converts to a `MonacoDiagnostic` through the existing
148/// diagnostics path: `Diagnostic::from(err)` then `.to_monaco(src)`. the
149/// source string is needed to translate the error's byte span into a 1-based
150/// line and column.
151fn compile_failed(errors: Vec<crate::errors::QalaError>, src: &str) -> CompileResult {
152    CompileResult {
153        ok: false,
154        disassembly: None,
155        diagnostics: errors
156            .into_iter()
157            .map(|e| crate::diagnostics::Diagnostic::from(e).to_monaco(src))
158            .collect(),
159    }
160}
161
162/// build the failed `Arm64Result` from a list of errors -- a front-end
163/// error or the backend's unsupported-construct rejection.
164///
165/// each `QalaError` converts to a `MonacoDiagnostic` through the same
166/// diagnostics path `compile_failed` uses: `Diagnostic::from(err)` then
167/// `.to_monaco(src)`. the source string translates the error's byte span
168/// into a 1-based line and column.
169fn arm64_failed(errors: Vec<crate::errors::QalaError>, src: &str) -> Arm64Result {
170    Arm64Result {
171        ok: false,
172        assembly: None,
173        diagnostics: errors
174            .into_iter()
175            .map(|e| crate::diagnostics::Diagnostic::from(e).to_monaco(src))
176            .collect(),
177    }
178}
179
180/// serialize a result struct to a `JsValue`.
181///
182/// on the practically unreachable serialization failure -- the result structs
183/// carry only `bool`, `String`, `Option`, `Vec`, and nested plain structs, so
184/// `serde_wasm_bindgen::to_value` does not fail for them -- this falls back to
185/// `JsValue::NULL` so the calling method still never panics and never throws.
186/// `JsValue::NULL` is the zero-dependency fallback; a richer error object
187/// would mean declaring `js-sys` directly.
188fn to_js<T: serde::Serialize>(value: &T) -> JsValue {
189    serde_wasm_bindgen::to_value(value).unwrap_or(JsValue::NULL)
190}
191
192/// the Qala browser session: one JavaScript-side object holding every piece
193/// of bridge state across calls.
194///
195/// the playground constructs one of these per browser tab, then calls methods
196/// on the returned object. the struct carries `#[wasm_bindgen]` so it crosses
197/// the boundary as a JavaScript class.
198#[wasm_bindgen]
199pub struct Qala {
200    /// the last successfully compiled and optimized program; `None` until the
201    /// first clean `compile`. `disassemble` reads it.
202    program: Option<crate::chunk::Program>,
203    /// the run-and-step VM over `program`, built fresh on each clean compile;
204    /// `None` until then. `run` and `step` advance it; `get_state` reads it.
205    vm: Option<crate::vm::Vm>,
206    /// the persistent REPL VM. constructed once by `Vm::new_repl` and kept for
207    /// the session so a binding or console line from one REPL call is visible
208    /// on the next.
209    repl_vm: crate::vm::Vm,
210    /// the source text of the last successful compile. kept so a runtime-error
211    /// diagnostic renders its source line against the exact text that produced
212    /// the bytecode.
213    last_src: String,
214}
215
216impl Default for Qala {
217    /// a default session is a fresh one -- delegates to [`Qala::new`].
218    ///
219    /// `new` carries `#[wasm_bindgen(constructor)]` and is the real entry
220    /// point; this impl exists so a default-constructible bound is satisfiable
221    /// in plain Rust.
222    fn default() -> Self {
223        Self::new()
224    }
225}
226
227impl Qala {
228    /// run the full compile pipeline on `source` and return a `CompileResult`.
229    ///
230    /// the pipeline is lex, parse, typecheck, codegen, then `Program::optimize`
231    /// followed by `Program::disassemble`; the disassembly is the optimized
232    /// bytecode, the same instructions `run` and `step` execute. on the first
233    /// lex, parse, type, or codegen error the method stops and returns
234    /// `ok: false` with the error diagnostics. on success it stores the
235    /// program, builds a fresh VM over the same source, records the source,
236    /// and returns `ok: true` with the disassembly and any warnings. never
237    /// panics, never throws.
238    fn compile_core(&mut self, source: &str) -> CompileResult {
239        // stage 1: lex. a single QalaError on failure.
240        let tokens = match crate::lexer::Lexer::tokenize(source) {
241            Ok(t) => t,
242            Err(e) => return compile_failed(vec![e], source),
243        };
244        // stage 2: parse. a single QalaError on failure.
245        let ast = match crate::parser::Parser::parse(&tokens) {
246            Ok(a) => a,
247            Err(e) => return compile_failed(vec![e], source),
248        };
249        // stage 3: typecheck. errors block; warnings never block.
250        let (typed, errors, warnings) = crate::typechecker::check_program(&ast, source);
251        if !errors.is_empty() {
252            return compile_failed(errors, source);
253        }
254        // stage 4: codegen. a Vec<QalaError> on failure.
255        let mut program = match crate::codegen::compile_program(&typed, source) {
256            Ok(p) => p,
257            Err(errs) => return compile_failed(errs, source),
258        };
259        // stage 5: optimize in place, then disassemble the OPTIMIZED program
260        // so the playground shows what run and step actually execute.
261        program.optimize();
262        let disassembly = program.disassemble();
263        // store the optimized program, a VM over it built with the SAME
264        // source, and the source itself -- all three together so a runtime
265        // error renders against the exact text that was compiled.
266        self.vm = Some(crate::vm::Vm::new(program.clone(), source.to_string()));
267        self.program = Some(program);
268        self.last_src = source.to_string();
269        CompileResult {
270            ok: true,
271            disassembly: Some(disassembly),
272            diagnostics: warnings
273                .iter()
274                .map(|w| crate::diagnostics::Diagnostic::from(w).to_monaco(source))
275                .collect(),
276        }
277    }
278
279    /// compile the given source through the ARM64 backend and return an
280    /// `Arm64Result`.
281    ///
282    /// runs lex, parse, and typecheck -- the same front end `compile_core`
283    /// runs -- then `crate::arm64::compile_arm64` on the typed AST. on a
284    /// front-end error returns `ok: false` with the front-end diagnostics; on
285    /// an unsupported-construct rejection returns `ok: false` with the
286    /// backend's diagnostics; on success returns `ok: true` with the assembly.
287    /// the source is an argument rather than `self.last_src` -- `last_src` is
288    /// set only on a successful `compile_core`, so reusing it would silently
289    /// compile stale source after a failed compile. never panics, never throws.
290    fn compile_arm64_core(&mut self, source: &str) -> Arm64Result {
291        // stage 1: lex. a single QalaError on failure.
292        let tokens = match crate::lexer::Lexer::tokenize(source) {
293            Ok(t) => t,
294            Err(e) => return arm64_failed(vec![e], source),
295        };
296        // stage 2: parse. a single QalaError on failure.
297        let ast = match crate::parser::Parser::parse(&tokens) {
298            Ok(a) => a,
299            Err(e) => return arm64_failed(vec![e], source),
300        };
301        // stage 3: typecheck. errors block; warnings never block.
302        let (typed, errors, _warnings) = crate::typechecker::check_program(&ast, source);
303        if !errors.is_empty() {
304            return arm64_failed(errors, source);
305        }
306        // stage 4: the ARM64 backend. an unsupported construct is Err(Vec<QalaError>).
307        match crate::arm64::compile_arm64(&typed, source) {
308            Ok(assembly) => Arm64Result {
309                ok: true,
310                assembly: Some(assembly),
311                diagnostics: Vec::new(),
312            },
313            Err(errs) => arm64_failed(errs, source),
314        }
315    }
316
317    /// run the compiled program to `Halt` or the first runtime error.
318    ///
319    /// with no program compiled this returns `ok: false`, an empty state, and
320    /// the no-program diagnostic. otherwise it runs the VM and returns the
321    /// final state either way -- on a runtime error the state is the
322    /// fault-point snapshot and `error` carries the diagnostic. never panics,
323    /// never throws.
324    fn run_core(&mut self) -> RunResult {
325        let vm = match self.vm.as_mut() {
326            Some(vm) => vm,
327            None => {
328                return RunResult {
329                    ok: false,
330                    state: empty_state(),
331                    error: Some(no_program_diagnostic()),
332                };
333            }
334        };
335        match vm.run() {
336            Ok(()) => RunResult {
337                ok: true,
338                state: vm.get_state(),
339                error: None,
340            },
341            Err(err) => RunResult {
342                ok: false,
343                // get_state after the error: the playground wants the stack
344                // and console at the fault point.
345                state: vm.get_state(),
346                error: Some(crate::diagnostics::Diagnostic::from(err).to_monaco(&self.last_src)),
347            },
348        }
349    }
350
351    /// advance the compiled program exactly one instruction.
352    ///
353    /// with no program compiled this returns `status: "error"`, an empty
354    /// state, and the no-program diagnostic. otherwise it steps the VM:
355    /// an ordinary instruction yields `"ran"`, reaching `Halt` yields
356    /// `"halted"`, and a runtime fault yields `"error"` with the diagnostic.
357    /// the state after the step is included in every compiled-program arm.
358    /// never panics, never throws.
359    fn step_core(&mut self) -> StepResult {
360        let vm = match self.vm.as_mut() {
361            Some(vm) => vm,
362            None => {
363                return StepResult {
364                    status: "error".to_string(),
365                    state: empty_state(),
366                    error: Some(no_program_diagnostic()),
367                };
368            }
369        };
370        match vm.step() {
371            Ok(crate::vm::StepOutcome::Ran) => StepResult {
372                status: "ran".to_string(),
373                state: vm.get_state(),
374                error: None,
375            },
376            Ok(crate::vm::StepOutcome::Halted) => StepResult {
377                status: "halted".to_string(),
378                state: vm.get_state(),
379                error: None,
380            },
381            Err(err) => StepResult {
382                status: "error".to_string(),
383                state: vm.get_state(),
384                error: Some(crate::diagnostics::Diagnostic::from(err).to_monaco(&self.last_src)),
385            },
386        }
387    }
388
389    /// snapshot the run-and-step VM's execution state.
390    ///
391    /// with no program compiled this returns the empty state rather than an
392    /// error -- the state of nothing is the zero snapshot. otherwise it
393    /// returns `Vm::get_state`. never panics, never throws.
394    fn get_state_core(&self) -> crate::vm::VmState {
395        self.vm
396            .as_ref()
397            .map(|vm| vm.get_state())
398            .unwrap_or_else(empty_state)
399    }
400
401    /// disassemble the compiled program's optimized bytecode.
402    ///
403    /// with no program compiled this returns a short placeholder line rather
404    /// than an error -- the disassembly of nothing is an empty listing. never
405    /// panics, never throws.
406    fn disassemble_core(&self) -> String {
407        self.program
408            .as_ref()
409            .map(|p| p.disassemble())
410            .unwrap_or_else(|| "; no program compiled -- call compile() first".to_string())
411    }
412
413    /// evaluate one line of REPL source against the persistent REPL VM.
414    ///
415    /// the REPL VM accumulates accepted lines, so a `let` binding from an
416    /// earlier call is in scope for this one and the console accumulates
417    /// across calls. on success the line's value is rendered to a
418    /// `StateValue`; on any error -- lex, parse, type, codegen, or runtime --
419    /// the diagnostic is returned and the line is not added to history, so a
420    /// typo cannot poison later calls. never panics, never throws.
421    fn repl_eval_core(&mut self, source: &str) -> ReplResult {
422        match self.repl_vm.repl_eval(source) {
423            Ok(value) => {
424                // value_to_string and runtime_type_name are pub(crate); wasm.rs
425                // is in the same crate, so it builds the StateValue directly --
426                // the identical pair Vm::get_state builds for each stack slot.
427                let rendered = self.repl_vm.value_to_string(value);
428                let type_name = self.repl_vm.runtime_type_name(value);
429                ReplResult {
430                    ok: true,
431                    value: Some(crate::vm::StateValue {
432                        rendered,
433                        type_name,
434                    }),
435                    console: self.repl_vm.get_state().console,
436                    error: None,
437                }
438            }
439            Err(err) => ReplResult {
440                ok: false,
441                value: None,
442                console: self.repl_vm.get_state().console,
443                // the REPL VM compiled a synthetic wrapped source the bridge
444                // does not hold, so a span into it cannot be rendered against
445                // last_src or the raw line. render against an empty source:
446                // the MonacoDiagnostic keeps the message -- the part the REPL
447                // console shows -- and the position collapses harmlessly.
448                error: Some(crate::diagnostics::Diagnostic::from(err).to_monaco("")),
449            },
450        }
451    }
452
453    /// clear all compiled and run state and rebuild a fresh REPL VM.
454    ///
455    /// after this the session is back to its just-constructed shape: no
456    /// program, no run-and-step VM, an empty REPL VM, and an empty last
457    /// source. never panics, never throws.
458    fn reset_core(&mut self) {
459        self.program = None;
460        self.vm = None;
461        self.repl_vm = crate::vm::Vm::new_repl();
462        self.last_src = String::new();
463    }
464}
465
466#[wasm_bindgen]
467impl Qala {
468    /// construct a fresh Qala session.
469    ///
470    /// installs the process-wide panic hook once, then returns a session with
471    /// no compiled program, no run-and-step VM, an empty REPL VM, and an empty
472    /// last source. the playground calls this once per browser tab. returns a
473    /// `Qala` that crosses the boundary as a JavaScript class.
474    #[wasm_bindgen(constructor)]
475    pub fn new() -> Qala {
476        install_panic_hook();
477        Qala {
478            program: None,
479            vm: None,
480            repl_vm: crate::vm::Vm::new_repl(),
481            last_src: String::new(),
482        }
483    }
484
485    /// compile Qala source and return a `CompileResult` as a `JsValue`.
486    ///
487    /// never throws -- a failed compile is `ok: false` data in the value.
488    pub fn compile(&mut self, source: &str) -> JsValue {
489        to_js(&self.compile_core(source))
490    }
491
492    /// compile the source through the ARM64 backend and return an
493    /// `Arm64Result` as a `JsValue`.
494    ///
495    /// never throws -- a front-end error or an unsupported construct is
496    /// `ok: false` data in the value.
497    pub fn compile_arm64(&mut self, source: &str) -> JsValue {
498        to_js(&self.compile_arm64_core(source))
499    }
500
501    /// run the compiled program and return a `RunResult` as a `JsValue`.
502    ///
503    /// never throws -- a runtime error or misuse is `ok: false` data in the
504    /// value.
505    pub fn run(&mut self) -> JsValue {
506        to_js(&self.run_core())
507    }
508
509    /// step the compiled program one instruction and return a `StepResult` as
510    /// a `JsValue`.
511    ///
512    /// never throws -- a runtime error or misuse is an `"error"` status in the
513    /// value.
514    pub fn step(&mut self) -> JsValue {
515        to_js(&self.step_core())
516    }
517
518    /// snapshot the VM state and return a `VmState` as a `JsValue`.
519    ///
520    /// never throws -- with no program compiled the value is the empty state.
521    pub fn get_state(&self) -> JsValue {
522        to_js(&self.get_state_core())
523    }
524
525    /// disassemble the compiled program and return the listing string as a
526    /// `JsValue`.
527    ///
528    /// never throws -- with no program compiled the value is a placeholder
529    /// listing.
530    pub fn disassemble(&self) -> JsValue {
531        to_js(&self.disassemble_core())
532    }
533
534    /// evaluate one REPL line and return a `ReplResult` as a `JsValue`.
535    ///
536    /// never throws -- a failed line is `ok: false` data in the value.
537    pub fn repl_eval(&mut self, source: &str) -> JsValue {
538        to_js(&self.repl_eval_core(source))
539    }
540
541    /// clear all session state and return `JsValue::NULL`.
542    ///
543    /// `reset` produces nothing meaningful -- the playground ignores the
544    /// return. never throws.
545    pub fn reset(&mut self) -> JsValue {
546        self.reset_core();
547        JsValue::NULL
548    }
549}
550
551/// install a panic hook once for the whole process.
552///
553/// the compiler and VM never panic by contract, so this hook is a bug-report
554/// safety net, not a feature: if the discipline is ever violated it captures
555/// the panic message into a process-local string for diagnosability rather
556/// than leaving an opaque WASM trap. the `Once` guard means repeated session
557/// constructions do not stack hooks.
558fn install_panic_hook() {
559    use std::sync::Once;
560    static HOOK: Once = Once::new();
561    HOOK.call_once(|| {
562        std::panic::set_hook(Box::new(|info| {
563            if let Ok(mut last) = LAST_PANIC.lock() {
564                *last = Some(info.to_string());
565            }
566        }));
567    });
568}
569
570/// the most recent panic message captured by the hook, or `None` if the
571/// no-panic contract has held. process-local; read only for bug reports.
572static LAST_PANIC: std::sync::Mutex<Option<String>> = std::sync::Mutex::new(None);
573
574#[cfg(test)]
575mod tests {
576    use super::*;
577
578    /// build a session and compile `source` against it, returning the session
579    /// ready for a run or step call. the compile is asserted to succeed so a
580    /// test that wants a compiled session does not silently get an
581    /// un-compiled one.
582    fn compiled(source: &str) -> Qala {
583        let mut qala = Qala::new();
584        let result = qala.compile_core(source);
585        assert!(
586            result.ok,
587            "fixture compile failed: {:?}",
588            result.diagnostics
589        );
590        qala
591    }
592
593    /// a small program that prints and halts -- the shared fixture for the
594    /// run, step, and reset tests.
595    const PRINTING_PROGRAM: &str = "fn main() is io {\n  println(\"step output\")\n}";
596
597    // ---- compile_core -----------------------------------------------------
598
599    #[test]
600    fn compile_core_on_a_clean_program_returns_ok_with_disassembly() {
601        let mut qala = Qala::new();
602        let result = qala.compile_core("fn main() is io {\n  println(\"hi\")\n}");
603        assert!(
604            result.ok,
605            "expected a clean compile: {:?}",
606            result.diagnostics
607        );
608        let disassembly = result.disassembly.expect("clean compile has disassembly");
609        assert!(!disassembly.is_empty(), "disassembly should not be empty");
610    }
611
612    #[test]
613    fn compile_core_on_a_broken_program_returns_the_error_diagnostics() {
614        let mut qala = Qala::new();
615        // line 2 assigns a str where the i64 annotation demands an i64.
616        let src = "fn main() is io {\n  let x: i64 = \"not a number\"\n}";
617        let result = qala.compile_core(src);
618        assert!(!result.ok, "a type error must fail the compile");
619        assert!(
620            result.disassembly.is_none(),
621            "a failed compile has no disassembly"
622        );
623        assert!(
624            !result.diagnostics.is_empty(),
625            "the error must surface as a diagnostic"
626        );
627        assert_eq!(
628            result.diagnostics[0].line, 2,
629            "the diagnostic should point at the offending line: {:?}",
630            result.diagnostics[0],
631        );
632    }
633
634    #[test]
635    fn compile_core_on_an_unused_variable_warns_but_compiles() {
636        let mut qala = Qala::new();
637        // `unused` is declared and never read -- an unused-variable warning.
638        let src = "fn main() is io {\n  let unused = 1\n  println(\"hi\")\n}";
639        let result = qala.compile_core(src);
640        assert!(result.ok, "a warning must not block the compile");
641        assert!(
642            result.disassembly.is_some(),
643            "a warning compile still disassembles"
644        );
645        assert!(
646            result.diagnostics.iter().any(|d| d.severity == 0),
647            "the unused-variable warning should be in the diagnostics: {:?}",
648            result.diagnostics,
649        );
650    }
651
652    // ---- compile_arm64_core -----------------------------------------------
653
654    #[test]
655    fn compile_arm64_core_on_an_integer_program_returns_ok_with_assembly() {
656        let mut qala = Qala::new();
657        // an integer-core program: a `let` and an i64-holed interpolation, both
658        // inside the ARM64 backend's shipped integer core.
659        let src = "fn main() is io {\n  let x = 1 + 2\n  println(\"{x}\")\n}";
660        let result = qala.compile_arm64_core(src);
661        assert!(
662            result.ok,
663            "expected ARM64 success: {:?}",
664            result.diagnostics
665        );
666        let assembly = result.assembly.expect("a success carries assembly");
667        assert!(
668            !assembly.is_empty(),
669            "the assembly text should not be empty"
670        );
671        assert!(
672            result.diagnostics.is_empty(),
673            "a success carries no diagnostics"
674        );
675    }
676
677    #[test]
678    fn compile_arm64_core_on_a_float_program_returns_the_backend_rejection() {
679        let mut qala = Qala::new();
680        // a float is outside the integer core -- the typechecker accepts it,
681        // the ARM64 backend rejects it with a clean diagnostic.
682        let result = qala.compile_arm64_core("fn main() is io {\n  let x = 1.5\n}");
683        assert!(!result.ok, "a float program must fail the ARM64 backend");
684        assert!(
685            result.assembly.is_none(),
686            "a rejected program has no assembly"
687        );
688        assert!(
689            !result.diagnostics.is_empty(),
690            "the rejection must surface a diagnostic"
691        );
692        let message = &result.diagnostics[0].message;
693        assert!(
694            message.contains("f64") || message.contains("floats"),
695            "the diagnostic should name the unsupported float construct: {message:?}",
696        );
697    }
698
699    #[test]
700    fn compile_arm64_core_on_a_broken_program_returns_not_ok() {
701        let mut qala = Qala::new();
702        // a syntactically broken program fails the front end before the backend.
703        let result = qala.compile_arm64_core("fn main( {");
704        assert!(!result.ok, "a broken program must fail the compile");
705        assert!(
706            result.assembly.is_none(),
707            "a failed compile has no assembly"
708        );
709        assert!(
710            !result.diagnostics.is_empty(),
711            "the syntax error must surface a diagnostic"
712        );
713    }
714
715    // ---- the WASM-05 end-to-end test --------------------------------------
716
717    #[test]
718    fn wasm_end_to_end_compiles_optimizes_and_runs_to_expected_output() {
719        // the hello.qala example content, inline so the test has no file
720        // dependency. compile_core runs the full pipeline including the
721        // optimizer; run_core executes the optimized program.
722        let mut qala = Qala::new();
723        let src = "fn main() is io {\n  let name = \"world\"\n  println(\"hello, {name}!\")\n}";
724        let compiled = qala.compile_core(src);
725        assert!(
726            compiled.ok,
727            "end-to-end compile failed: {:?}",
728            compiled.diagnostics
729        );
730        let run = qala.run_core();
731        assert!(run.ok, "end-to-end run failed: {:?}", run.error);
732        assert!(
733            run.state
734                .console
735                .iter()
736                .any(|l| l.contains("hello, world!")),
737            "console did not contain the expected output: {:?}",
738            run.state.console,
739        );
740    }
741
742    // ---- misuse before compile --------------------------------------------
743
744    #[test]
745    fn run_core_before_compile_returns_an_error_shaped_result() {
746        let mut qala = Qala::new();
747        let result = qala.run_core();
748        assert!(!result.ok, "run before compile must report failure");
749        assert!(
750            result.error.is_some(),
751            "run before compile must carry a diagnostic"
752        );
753    }
754
755    #[test]
756    fn step_core_before_compile_returns_status_error() {
757        let mut qala = Qala::new();
758        let result = qala.step_core();
759        assert_eq!(
760            result.status, "error",
761            "step before compile must be an error"
762        );
763        assert!(
764            result.error.is_some(),
765            "step before compile must carry a diagnostic"
766        );
767    }
768
769    #[test]
770    fn get_state_core_before_compile_returns_an_empty_state() {
771        let qala = Qala::new();
772        let state = qala.get_state_core();
773        assert!(
774            state.stack.is_empty(),
775            "an un-compiled session has no stack"
776        );
777        assert!(
778            state.console.is_empty(),
779            "an un-compiled session has no console"
780        );
781    }
782
783    #[test]
784    fn disassemble_core_before_compile_returns_a_placeholder() {
785        let qala = Qala::new();
786        let listing = qala.disassemble_core();
787        assert!(!listing.is_empty(), "the placeholder listing is non-empty");
788        assert!(
789            listing.contains("no program compiled"),
790            "the placeholder should mention no program: {listing:?}",
791        );
792    }
793
794    // ---- run and step -----------------------------------------------------
795
796    #[test]
797    fn step_core_advances_then_halts() {
798        let mut qala = compiled(PRINTING_PROGRAM);
799        let mut saw_ran = false;
800        // step through the program; a tiny program halts well within this cap.
801        for _ in 0..1000 {
802            let result = qala.step_core();
803            match result.status.as_str() {
804                "ran" => saw_ran = true,
805                "halted" => {
806                    assert!(saw_ran, "a program should run at least one instruction");
807                    return;
808                }
809                other => panic!("unexpected step status: {other}"),
810            }
811        }
812        panic!("the program did not halt within the step cap");
813    }
814
815    // ---- the REPL ---------------------------------------------------------
816
817    #[test]
818    fn repl_eval_core_persists_a_binding_across_calls() {
819        let mut qala = Qala::new();
820        let first = qala.repl_eval_core("let x = 5");
821        assert!(
822            first.ok,
823            "the binding line should evaluate: {:?}",
824            first.error
825        );
826        let second = qala.repl_eval_core("x + 1");
827        assert!(
828            second.ok,
829            "the binding should be visible on the next call: {:?}",
830            second.error
831        );
832        let value = second.value.expect("an expression line has a value");
833        assert_eq!(value.rendered, "6", "x + 1 should render as 6");
834    }
835
836    #[test]
837    fn repl_eval_core_console_persists_across_calls() {
838        let mut qala = Qala::new();
839        let first = qala.repl_eval_core("println(\"one\")");
840        assert!(
841            first.ok,
842            "the first println line should evaluate: {:?}",
843            first.error
844        );
845        let first_len = first.console.len();
846        assert!(
847            first_len > 0,
848            "the first println should produce console output"
849        );
850        let second = qala.repl_eval_core("println(\"two\")");
851        assert!(
852            second.ok,
853            "the second println line should evaluate: {:?}",
854            second.error
855        );
856        assert!(
857            second.console.len() > first_len,
858            "the console should accumulate across calls: {:?}",
859            second.console,
860        );
861        assert!(
862            second.console.iter().any(|l| l.contains("one")),
863            "the earlier output should still be present: {:?}",
864            second.console,
865        );
866    }
867
868    // ---- reset ------------------------------------------------------------
869
870    #[test]
871    fn reset_core_clears_compiled_state() {
872        let mut qala = compiled(PRINTING_PROGRAM);
873        let run = qala.run_core();
874        assert!(run.ok, "the fixture program should run: {:?}", run.error);
875        qala.reset_core();
876        let state = qala.get_state_core();
877        assert!(state.stack.is_empty(), "reset should clear the stack");
878        assert!(state.console.is_empty(), "reset should clear the console");
879    }
880
881    // ---- the serialize witness --------------------------------------------
882
883    #[test]
884    fn result_structs_implement_serialize() {
885        // a compile-time check: if a result struct stops deriving
886        // serde::Serialize this generic call fails to typecheck and the test
887        // build breaks. Serialize has a generic method so it is not
888        // dyn-compatible; a generic asserting function is the standard way to
889        // spell "T: Serialize" as a witness.
890        fn assert_serialize<T: serde::Serialize>(_: &T) {}
891        assert_serialize(&CompileResult {
892            ok: true,
893            disassembly: None,
894            diagnostics: Vec::new(),
895        });
896        assert_serialize(&RunResult {
897            ok: true,
898            state: empty_state(),
899            error: None,
900        });
901        assert_serialize(&StepResult {
902            status: "ran".to_string(),
903            state: empty_state(),
904            error: None,
905        });
906        assert_serialize(&ReplResult {
907            ok: true,
908            value: None,
909            console: Vec::new(),
910            error: None,
911        });
912        assert_serialize(&Arm64Result {
913            ok: true,
914            assembly: None,
915            diagnostics: Vec::new(),
916        });
917    }
918}