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}