use wasm_bindgen::prelude::*;
#[derive(Debug, Clone, serde::Serialize)]
pub struct CompileResult {
pub ok: bool,
pub disassembly: Option<String>,
pub diagnostics: Vec<crate::diagnostics::MonacoDiagnostic>,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct Arm64Result {
pub ok: bool,
pub assembly: Option<String>,
pub diagnostics: Vec<crate::diagnostics::MonacoDiagnostic>,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct RunResult {
pub ok: bool,
pub state: crate::vm::VmState,
pub error: Option<crate::diagnostics::MonacoDiagnostic>,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct StepResult {
pub status: String,
pub state: crate::vm::VmState,
pub error: Option<crate::diagnostics::MonacoDiagnostic>,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct ReplResult {
pub ok: bool,
pub value: Option<crate::vm::StateValue>,
pub console: Vec<String>,
pub error: Option<crate::diagnostics::MonacoDiagnostic>,
}
fn empty_state() -> crate::vm::VmState {
crate::vm::VmState {
chunk_index: 0,
ip: 0,
current_line: 0,
stack: Vec::new(),
variables: Vec::new(),
console: Vec::new(),
leak_log: Vec::new(),
}
}
fn no_program_diagnostic() -> crate::diagnostics::MonacoDiagnostic {
crate::diagnostics::MonacoDiagnostic {
line: 1,
column: 1,
end_line: 1,
end_column: 1,
severity: 1,
message: "no program compiled -- call compile() first".to_string(),
category: None,
}
}
fn compile_failed(errors: Vec<crate::errors::QalaError>, src: &str) -> CompileResult {
CompileResult {
ok: false,
disassembly: None,
diagnostics: errors
.into_iter()
.map(|e| crate::diagnostics::Diagnostic::from(e).to_monaco(src))
.collect(),
}
}
fn arm64_failed(errors: Vec<crate::errors::QalaError>, src: &str) -> Arm64Result {
Arm64Result {
ok: false,
assembly: None,
diagnostics: errors
.into_iter()
.map(|e| crate::diagnostics::Diagnostic::from(e).to_monaco(src))
.collect(),
}
}
fn to_js<T: serde::Serialize>(value: &T) -> JsValue {
serde_wasm_bindgen::to_value(value).unwrap_or(JsValue::NULL)
}
#[wasm_bindgen]
pub struct Qala {
program: Option<crate::chunk::Program>,
vm: Option<crate::vm::Vm>,
repl_vm: crate::vm::Vm,
last_src: String,
}
impl Default for Qala {
fn default() -> Self {
Self::new()
}
}
impl Qala {
fn compile_core(&mut self, source: &str) -> CompileResult {
let tokens = match crate::lexer::Lexer::tokenize(source) {
Ok(t) => t,
Err(e) => return compile_failed(vec![e], source),
};
let ast = match crate::parser::Parser::parse(&tokens) {
Ok(a) => a,
Err(e) => return compile_failed(vec![e], source),
};
let (typed, errors, warnings) = crate::typechecker::check_program(&ast, source);
if !errors.is_empty() {
return compile_failed(errors, source);
}
let mut program = match crate::codegen::compile_program(&typed, source) {
Ok(p) => p,
Err(errs) => return compile_failed(errs, source),
};
program.optimize();
let disassembly = program.disassemble();
self.vm = Some(crate::vm::Vm::new(program.clone(), source.to_string()));
self.program = Some(program);
self.last_src = source.to_string();
CompileResult {
ok: true,
disassembly: Some(disassembly),
diagnostics: warnings
.iter()
.map(|w| crate::diagnostics::Diagnostic::from(w).to_monaco(source))
.collect(),
}
}
fn compile_arm64_core(&mut self, source: &str) -> Arm64Result {
let tokens = match crate::lexer::Lexer::tokenize(source) {
Ok(t) => t,
Err(e) => return arm64_failed(vec![e], source),
};
let ast = match crate::parser::Parser::parse(&tokens) {
Ok(a) => a,
Err(e) => return arm64_failed(vec![e], source),
};
let (typed, errors, _warnings) = crate::typechecker::check_program(&ast, source);
if !errors.is_empty() {
return arm64_failed(errors, source);
}
match crate::arm64::compile_arm64(&typed, source) {
Ok(assembly) => Arm64Result {
ok: true,
assembly: Some(assembly),
diagnostics: Vec::new(),
},
Err(errs) => arm64_failed(errs, source),
}
}
fn run_core(&mut self) -> RunResult {
let vm = match self.vm.as_mut() {
Some(vm) => vm,
None => {
return RunResult {
ok: false,
state: empty_state(),
error: Some(no_program_diagnostic()),
};
}
};
match vm.run() {
Ok(()) => RunResult {
ok: true,
state: vm.get_state(),
error: None,
},
Err(err) => RunResult {
ok: false,
state: vm.get_state(),
error: Some(crate::diagnostics::Diagnostic::from(err).to_monaco(&self.last_src)),
},
}
}
fn step_core(&mut self) -> StepResult {
let vm = match self.vm.as_mut() {
Some(vm) => vm,
None => {
return StepResult {
status: "error".to_string(),
state: empty_state(),
error: Some(no_program_diagnostic()),
};
}
};
match vm.step() {
Ok(crate::vm::StepOutcome::Ran) => StepResult {
status: "ran".to_string(),
state: vm.get_state(),
error: None,
},
Ok(crate::vm::StepOutcome::Halted) => StepResult {
status: "halted".to_string(),
state: vm.get_state(),
error: None,
},
Err(err) => StepResult {
status: "error".to_string(),
state: vm.get_state(),
error: Some(crate::diagnostics::Diagnostic::from(err).to_monaco(&self.last_src)),
},
}
}
fn get_state_core(&self) -> crate::vm::VmState {
self.vm
.as_ref()
.map(|vm| vm.get_state())
.unwrap_or_else(empty_state)
}
fn disassemble_core(&self) -> String {
self.program
.as_ref()
.map(|p| p.disassemble())
.unwrap_or_else(|| "; no program compiled -- call compile() first".to_string())
}
fn repl_eval_core(&mut self, source: &str) -> ReplResult {
match self.repl_vm.repl_eval(source) {
Ok(value) => {
let rendered = self.repl_vm.value_to_string(value);
let type_name = self.repl_vm.runtime_type_name(value);
ReplResult {
ok: true,
value: Some(crate::vm::StateValue {
rendered,
type_name,
}),
console: self.repl_vm.get_state().console,
error: None,
}
}
Err(err) => ReplResult {
ok: false,
value: None,
console: self.repl_vm.get_state().console,
error: Some(crate::diagnostics::Diagnostic::from(err).to_monaco("")),
},
}
}
fn reset_core(&mut self) {
self.program = None;
self.vm = None;
self.repl_vm = crate::vm::Vm::new_repl();
self.last_src = String::new();
}
}
#[wasm_bindgen]
impl Qala {
#[wasm_bindgen(constructor)]
pub fn new() -> Qala {
install_panic_hook();
Qala {
program: None,
vm: None,
repl_vm: crate::vm::Vm::new_repl(),
last_src: String::new(),
}
}
pub fn compile(&mut self, source: &str) -> JsValue {
to_js(&self.compile_core(source))
}
pub fn compile_arm64(&mut self, source: &str) -> JsValue {
to_js(&self.compile_arm64_core(source))
}
pub fn run(&mut self) -> JsValue {
to_js(&self.run_core())
}
pub fn step(&mut self) -> JsValue {
to_js(&self.step_core())
}
pub fn get_state(&self) -> JsValue {
to_js(&self.get_state_core())
}
pub fn disassemble(&self) -> JsValue {
to_js(&self.disassemble_core())
}
pub fn repl_eval(&mut self, source: &str) -> JsValue {
to_js(&self.repl_eval_core(source))
}
pub fn reset(&mut self) -> JsValue {
self.reset_core();
JsValue::NULL
}
}
fn install_panic_hook() {
use std::sync::Once;
static HOOK: Once = Once::new();
HOOK.call_once(|| {
std::panic::set_hook(Box::new(|info| {
if let Ok(mut last) = LAST_PANIC.lock() {
*last = Some(info.to_string());
}
}));
});
}
static LAST_PANIC: std::sync::Mutex<Option<String>> = std::sync::Mutex::new(None);
#[cfg(test)]
mod tests {
use super::*;
fn compiled(source: &str) -> Qala {
let mut qala = Qala::new();
let result = qala.compile_core(source);
assert!(
result.ok,
"fixture compile failed: {:?}",
result.diagnostics
);
qala
}
const PRINTING_PROGRAM: &str = "fn main() is io {\n println(\"step output\")\n}";
#[test]
fn compile_core_on_a_clean_program_returns_ok_with_disassembly() {
let mut qala = Qala::new();
let result = qala.compile_core("fn main() is io {\n println(\"hi\")\n}");
assert!(
result.ok,
"expected a clean compile: {:?}",
result.diagnostics
);
let disassembly = result.disassembly.expect("clean compile has disassembly");
assert!(!disassembly.is_empty(), "disassembly should not be empty");
}
#[test]
fn compile_core_on_a_broken_program_returns_the_error_diagnostics() {
let mut qala = Qala::new();
let src = "fn main() is io {\n let x: i64 = \"not a number\"\n}";
let result = qala.compile_core(src);
assert!(!result.ok, "a type error must fail the compile");
assert!(
result.disassembly.is_none(),
"a failed compile has no disassembly"
);
assert!(
!result.diagnostics.is_empty(),
"the error must surface as a diagnostic"
);
assert_eq!(
result.diagnostics[0].line, 2,
"the diagnostic should point at the offending line: {:?}",
result.diagnostics[0],
);
}
#[test]
fn compile_core_on_an_unused_variable_warns_but_compiles() {
let mut qala = Qala::new();
let src = "fn main() is io {\n let unused = 1\n println(\"hi\")\n}";
let result = qala.compile_core(src);
assert!(result.ok, "a warning must not block the compile");
assert!(
result.disassembly.is_some(),
"a warning compile still disassembles"
);
assert!(
result.diagnostics.iter().any(|d| d.severity == 0),
"the unused-variable warning should be in the diagnostics: {:?}",
result.diagnostics,
);
}
#[test]
fn compile_arm64_core_on_an_integer_program_returns_ok_with_assembly() {
let mut qala = Qala::new();
let src = "fn main() is io {\n let x = 1 + 2\n println(\"{x}\")\n}";
let result = qala.compile_arm64_core(src);
assert!(
result.ok,
"expected ARM64 success: {:?}",
result.diagnostics
);
let assembly = result.assembly.expect("a success carries assembly");
assert!(
!assembly.is_empty(),
"the assembly text should not be empty"
);
assert!(
result.diagnostics.is_empty(),
"a success carries no diagnostics"
);
}
#[test]
fn compile_arm64_core_on_a_float_program_returns_the_backend_rejection() {
let mut qala = Qala::new();
let result = qala.compile_arm64_core("fn main() is io {\n let x = 1.5\n}");
assert!(!result.ok, "a float program must fail the ARM64 backend");
assert!(
result.assembly.is_none(),
"a rejected program has no assembly"
);
assert!(
!result.diagnostics.is_empty(),
"the rejection must surface a diagnostic"
);
let message = &result.diagnostics[0].message;
assert!(
message.contains("f64") || message.contains("floats"),
"the diagnostic should name the unsupported float construct: {message:?}",
);
}
#[test]
fn compile_arm64_core_on_a_broken_program_returns_not_ok() {
let mut qala = Qala::new();
let result = qala.compile_arm64_core("fn main( {");
assert!(!result.ok, "a broken program must fail the compile");
assert!(
result.assembly.is_none(),
"a failed compile has no assembly"
);
assert!(
!result.diagnostics.is_empty(),
"the syntax error must surface a diagnostic"
);
}
#[test]
fn wasm_end_to_end_compiles_optimizes_and_runs_to_expected_output() {
let mut qala = Qala::new();
let src = "fn main() is io {\n let name = \"world\"\n println(\"hello, {name}!\")\n}";
let compiled = qala.compile_core(src);
assert!(
compiled.ok,
"end-to-end compile failed: {:?}",
compiled.diagnostics
);
let run = qala.run_core();
assert!(run.ok, "end-to-end run failed: {:?}", run.error);
assert!(
run.state
.console
.iter()
.any(|l| l.contains("hello, world!")),
"console did not contain the expected output: {:?}",
run.state.console,
);
}
#[test]
fn run_core_before_compile_returns_an_error_shaped_result() {
let mut qala = Qala::new();
let result = qala.run_core();
assert!(!result.ok, "run before compile must report failure");
assert!(
result.error.is_some(),
"run before compile must carry a diagnostic"
);
}
#[test]
fn step_core_before_compile_returns_status_error() {
let mut qala = Qala::new();
let result = qala.step_core();
assert_eq!(
result.status, "error",
"step before compile must be an error"
);
assert!(
result.error.is_some(),
"step before compile must carry a diagnostic"
);
}
#[test]
fn get_state_core_before_compile_returns_an_empty_state() {
let qala = Qala::new();
let state = qala.get_state_core();
assert!(
state.stack.is_empty(),
"an un-compiled session has no stack"
);
assert!(
state.console.is_empty(),
"an un-compiled session has no console"
);
}
#[test]
fn disassemble_core_before_compile_returns_a_placeholder() {
let qala = Qala::new();
let listing = qala.disassemble_core();
assert!(!listing.is_empty(), "the placeholder listing is non-empty");
assert!(
listing.contains("no program compiled"),
"the placeholder should mention no program: {listing:?}",
);
}
#[test]
fn step_core_advances_then_halts() {
let mut qala = compiled(PRINTING_PROGRAM);
let mut saw_ran = false;
for _ in 0..1000 {
let result = qala.step_core();
match result.status.as_str() {
"ran" => saw_ran = true,
"halted" => {
assert!(saw_ran, "a program should run at least one instruction");
return;
}
other => panic!("unexpected step status: {other}"),
}
}
panic!("the program did not halt within the step cap");
}
#[test]
fn repl_eval_core_persists_a_binding_across_calls() {
let mut qala = Qala::new();
let first = qala.repl_eval_core("let x = 5");
assert!(
first.ok,
"the binding line should evaluate: {:?}",
first.error
);
let second = qala.repl_eval_core("x + 1");
assert!(
second.ok,
"the binding should be visible on the next call: {:?}",
second.error
);
let value = second.value.expect("an expression line has a value");
assert_eq!(value.rendered, "6", "x + 1 should render as 6");
}
#[test]
fn repl_eval_core_console_persists_across_calls() {
let mut qala = Qala::new();
let first = qala.repl_eval_core("println(\"one\")");
assert!(
first.ok,
"the first println line should evaluate: {:?}",
first.error
);
let first_len = first.console.len();
assert!(
first_len > 0,
"the first println should produce console output"
);
let second = qala.repl_eval_core("println(\"two\")");
assert!(
second.ok,
"the second println line should evaluate: {:?}",
second.error
);
assert!(
second.console.len() > first_len,
"the console should accumulate across calls: {:?}",
second.console,
);
assert!(
second.console.iter().any(|l| l.contains("one")),
"the earlier output should still be present: {:?}",
second.console,
);
}
#[test]
fn reset_core_clears_compiled_state() {
let mut qala = compiled(PRINTING_PROGRAM);
let run = qala.run_core();
assert!(run.ok, "the fixture program should run: {:?}", run.error);
qala.reset_core();
let state = qala.get_state_core();
assert!(state.stack.is_empty(), "reset should clear the stack");
assert!(state.console.is_empty(), "reset should clear the console");
}
#[test]
fn result_structs_implement_serialize() {
fn assert_serialize<T: serde::Serialize>(_: &T) {}
assert_serialize(&CompileResult {
ok: true,
disassembly: None,
diagnostics: Vec::new(),
});
assert_serialize(&RunResult {
ok: true,
state: empty_state(),
error: None,
});
assert_serialize(&StepResult {
status: "ran".to_string(),
state: empty_state(),
error: None,
});
assert_serialize(&ReplResult {
ok: true,
value: None,
console: Vec::new(),
error: None,
});
assert_serialize(&Arm64Result {
ok: true,
assembly: None,
diagnostics: Vec::new(),
});
}
}