Skip to main content

qala_compiler/
stdlib.rs

1//! the native-Rust standard library.
2//!
3//! Qala's standard library is fifteen functions implemented directly in Rust,
4//! not in Qala. codegen assigns each one a reserved fn-id at or above
5//! [`STDLIB_FN_BASE`] (40000); the VM's `CALL` handler routes any such fn-id
6//! here through [`dispatch`]. user functions get dense ids `0..N` into
7//! `Program.chunks`; the two id spaces never overlap.
8//!
9//! the functions, in the order their fn-ids are assigned (the order MUST match
10//! `codegen.rs`'s `STDLIB_TABLE` -- entry `i` is fn-id `STDLIB_FN_BASE + i`):
11//!
12//! | fn-id | name       | effect | what it does |
13//! |-------|------------|--------|--------------|
14//! | 40000 | `print`    | io     | render the argument, append to the console |
15//! | 40001 | `println`  | io     | render the argument, append a console line |
16//! | 40002 | `sqrt`     | pure   | `f64` square root |
17//! | 40003 | `abs`      | pure   | absolute value of an `i64` or an `f64` |
18//! | 40004 | `assert`   | panic  | a `false` argument is a runtime error |
19//! | 40005 | `len`      | pure   | element count of an array, or char count of a string |
20//! | 40006 | `push`     | alloc  | append a value to a heap array |
21//! | 40007 | `pop`      | alloc  | remove the last element, return `Option<T>` |
22//! | 40008 | `type_of`  | pure   | the runtime type name as a string |
23//! | 40009 | `open`     | io     | open a mock file handle |
24//! | 40010 | `close`    | io     | mark a file handle closed |
25//! | 40011 | `map`      | pure*  | apply a callback to each array element |
26//! | 40012 | `filter`   | pure*  | keep the elements a callback accepts |
27//! | 40013 | `reduce`   | pure*  | fold an array with a callback |
28//! | 40014 | `read_all` | io     | read a file handle's content (`FileHandle.read_all`) |
29//!
30//! (`map` / `filter` / `reduce` inherit the callback's effect; Phase 3's effect
31//! inference already propagated that into the caller, so the native code here
32//! does not re-check effects.)
33//!
34//! the effect column is consistent with the typechecker's `stdlib_signatures`
35//! table -- same fifteen functions, same effects. the typechecker owns the
36//! signature; this module owns the runtime behavior.
37//!
38//! file I/O is a mock. `open` does NOT touch a real filesystem: the VM compiles
39//! to WASM and runs in a browser sandbox where there is no filesystem to reach.
40//! `open` builds a [`HeapObject::FileHandle`] with the requested path and an
41//! empty mock content string; `close` flips its `closed` flag; `read_all`
42//! returns the (empty) content as `Ok`. this is a deliberate v1 limitation --
43//! it makes the effect system and `defer close(f)` demonstrable in the
44//! playground without a real file API. `print` / `println` likewise write to
45//! the VM's console buffer, not real stdout.
46//!
47//! every function is total against untrusted bytecode: the fn-id and the
48//! argument values come from a `CALL` instruction the VM does not trust, so a
49//! wrong-arity or wrong-type call is a [`QalaError::Runtime`], never a panic.
50//! [`dispatch`] rejects an unknown fn-id the same way.
51
52use crate::errors::QalaError;
53use crate::opcode::STDLIB_FN_BASE;
54use crate::value::Value;
55use crate::vm::{HeapObject, Vm};
56
57/// the `MAKE_ENUM_VARIANT` id of `Result::Ok`.
58///
59/// codegen pre-registers the four built-in `Result` / `Option` variants in
60/// `Program.enum_variant_names` before any user variant: `Ok` = 0, `Err` = 1,
61/// `Some` = 2, `None` = 3. `read_all` builds an `Ok` value and `pop` builds a
62/// `Some` / `None` value directly as [`HeapObject::EnumVariant`]s, so they
63/// carry the variant names rather than these ids -- the ids are recorded here
64/// only to document the codegen contract the names mirror.
65#[allow(dead_code)]
66const VARIANT_ID_OK: u16 = 0;
67
68/// dispatch a stdlib `CALL` to its native implementation.
69///
70/// `fn_id` is the `u16` from the `CALL` opcode; the VM routes any `fn_id` at or
71/// above [`STDLIB_FN_BASE`] here. `args` are the call's argument values in
72/// source order -- `args[0]` is the leftmost argument (for a method call such
73/// as `FileHandle.read_all`, `args[0]` is the receiver). the returned [`Value`]
74/// is what the VM pushes onto the value stack for the instruction after the
75/// `CALL`; a `void`-returning function returns [`Value::void`].
76///
77/// the `match` lists the functions in the exact `STDLIB_TABLE` order: entry `i`
78/// in that table is `STDLIB_FN_BASE + i`, so `print` is 40000, `read_all` is
79/// 40014. a `fn_id` below [`STDLIB_FN_BASE`] reaching here is a VM-internal
80/// invariant violation (the `CALL` handler should have routed it to a user
81/// frame); an unknown `fn_id` at or above the base is malformed bytecode.
82/// both are a clean [`QalaError::Runtime`], never a panic.
83pub fn dispatch(vm: &mut Vm, fn_id: u16, args: &[Value]) -> Result<Value, QalaError> {
84    if fn_id < STDLIB_FN_BASE {
85        return Err(vm.runtime_err(&format!("internal error: fn-id {fn_id} is not a stdlib id")));
86    }
87    match fn_id {
88        40000 => print(vm, args),
89        40001 => println(vm, args),
90        40002 => sqrt(vm, args),
91        40003 => abs(vm, args),
92        40004 => assert(vm, args),
93        40005 => len(vm, args),
94        40006 => push(vm, args),
95        40007 => pop(vm, args),
96        40008 => type_of(vm, args),
97        40009 => open(vm, args),
98        40010 => close(vm, args),
99        40011 => map(vm, args),
100        40012 => filter(vm, args),
101        40013 => reduce(vm, args),
102        40014 => read_all(vm, args),
103        other => Err(vm.runtime_err(&format!("unknown stdlib function {other}"))),
104    }
105}
106
107// ---- argument helpers ------------------------------------------------------
108
109/// the single argument of a one-argument stdlib function.
110///
111/// a wrong argument count is malformed bytecode (the typechecker checked the
112/// arity, so this only fires on a hand-crafted or corrupt `CALL`) -- a clean
113/// `Runtime` error, never an index panic.
114fn one_arg(vm: &Vm, args: &[Value], name: &str) -> Result<Value, QalaError> {
115    match args {
116        [a] => Ok(*a),
117        _ => Err(vm.runtime_err(&format!("{name} expects 1 argument, got {}", args.len()))),
118    }
119}
120
121/// allocate a [`HeapObject::Int`] for `n` and return a pointer to it. a heap
122/// exhaustion is a `Runtime` error.
123fn alloc_int(vm: &mut Vm, n: i64) -> Result<Value, QalaError> {
124    let slot = vm
125        .heap
126        .alloc(HeapObject::Int(n))
127        .ok_or_else(|| vm.runtime_err("heap exhausted"))?;
128    Ok(Value::pointer(slot))
129}
130
131/// allocate a [`HeapObject::Str`] for `s` and return a pointer to it. a heap
132/// exhaustion is a `Runtime` error.
133fn alloc_str(vm: &mut Vm, s: String) -> Result<Value, QalaError> {
134    let slot = vm
135        .heap
136        .alloc(HeapObject::Str(s))
137        .ok_or_else(|| vm.runtime_err("heap exhausted"))?;
138    Ok(Value::pointer(slot))
139}
140
141/// allocate a [`HeapObject::Array`] for `items` and return a pointer to it. a
142/// heap exhaustion is a `Runtime` error.
143fn alloc_array(vm: &mut Vm, items: Vec<Value>) -> Result<Value, QalaError> {
144    let slot = vm
145        .heap
146        .alloc(HeapObject::Array(items))
147        .ok_or_else(|| vm.runtime_err("heap exhausted"))?;
148    Ok(Value::pointer(slot))
149}
150
151/// build an `Option` enum-variant value: `Some(payload)` when `payload` is
152/// `Some`, `None` when it is `None`.
153///
154/// codegen registers `Some` / `None` as built-in `Option` variants; the heap
155/// object carries the `(enum, variant)` names, which is what `MATCH_VARIANT`
156/// and the value renderer key on. a heap exhaustion is a `Runtime` error.
157fn make_option(vm: &mut Vm, payload: Option<Value>) -> Result<Value, QalaError> {
158    let object = match payload {
159        Some(v) => HeapObject::EnumVariant {
160            type_name: "Option".to_string(),
161            variant: "Some".to_string(),
162            payload: vec![v],
163        },
164        None => HeapObject::EnumVariant {
165            type_name: "Option".to_string(),
166            variant: "None".to_string(),
167            payload: Vec::new(),
168        },
169    };
170    let slot = vm
171        .heap
172        .alloc(object)
173        .ok_or_else(|| vm.runtime_err("heap exhausted"))?;
174    Ok(Value::pointer(slot))
175}
176
177/// build a `Result::Ok(payload)` enum-variant value. used by `read_all`. a heap
178/// exhaustion is a `Runtime` error.
179fn make_ok(vm: &mut Vm, payload: Value) -> Result<Value, QalaError> {
180    let slot = vm
181        .heap
182        .alloc(HeapObject::EnumVariant {
183            type_name: "Result".to_string(),
184            variant: "Ok".to_string(),
185            payload: vec![payload],
186        })
187        .ok_or_else(|| vm.runtime_err("heap exhausted"))?;
188    Ok(Value::pointer(slot))
189}
190
191// ---- the fifteen native functions ------------------------------------------
192
193/// `print(x)` -- render `x` and append it to the VM console.
194///
195/// the console is a `Vec<String>` -- one entry per call. `print` pushes its
196/// rendered text as its own entry. `print` is specified as the no-newline
197/// counterpart of `println`, but the v1 console buffer is a flat list of
198/// strings with no sub-line cursor, so a `print` entry is shown as its own
199/// console line; true mid-line concatenation (`print("a"); print("b")`
200/// rendering as one line `ab`) is a documented v1 limitation -- a console with
201/// a line cursor is a future enhancement. the bundled examples use only
202/// `println`. returns `void`.
203fn print(vm: &mut Vm, args: &[Value]) -> Result<Value, QalaError> {
204    let arg = one_arg(vm, args, "print")?;
205    let text = vm.value_to_string(arg);
206    vm.console.push(text);
207    Ok(Value::void())
208}
209
210/// `println(x)` -- render `x` and append it to the VM console as a line.
211///
212/// each `println` pushes one console entry with a trailing `'\n'`. the
213/// playground renders one entry per line, so the newline is typically invisible
214/// when the console displays entries individually, but consumers that
215/// concatenate entries into a single text block get the correct newline
216/// separation between lines (and no trailing newline after `print` entries).
217/// returns `void`.
218fn println(vm: &mut Vm, args: &[Value]) -> Result<Value, QalaError> {
219    let arg = one_arg(vm, args, "println")?;
220    let mut text = vm.value_to_string(arg);
221    text.push('\n');
222    vm.console.push(text);
223    Ok(Value::void())
224}
225
226/// `sqrt(x: f64) -> f64` -- the IEEE 754 square root.
227///
228/// a negative argument yields `NaN` (IEEE 754), not an error. a non-`f64`
229/// argument is a `Runtime` error.
230fn sqrt(vm: &mut Vm, args: &[Value]) -> Result<Value, QalaError> {
231    let arg = one_arg(vm, args, "sqrt")?;
232    let x = arg
233        .as_f64()
234        .ok_or_else(|| vm.runtime_err("sqrt: expected a float"))?;
235    Ok(Value::from_f64(x.sqrt()))
236}
237
238/// `abs(x)` -- the absolute value of an `i64` or an `f64`.
239///
240/// `abs` is generic: the typechecker resolves the return type to the argument
241/// type, so the native code inspects the runtime kind. an `i64` argument (a
242/// pointer to a heap `Int`) returns a heap `Int`; `i64::MIN` overflows `i64`'s
243/// range and is a `Runtime` error rather than a wrap. an `f64` argument returns
244/// an `f64`. any other kind is a `Runtime` error.
245fn abs(vm: &mut Vm, args: &[Value]) -> Result<Value, QalaError> {
246    let arg = one_arg(vm, args, "abs")?;
247    // an f64 is stored inline; try that first.
248    if let Some(x) = arg.as_f64() {
249        return Ok(Value::from_f64(x.abs()));
250    }
251    // otherwise the argument must be a pointer to a heap Int.
252    let int = arg
253        .as_pointer()
254        .and_then(|slot| match vm.heap.get(slot) {
255            Some(HeapObject::Int(n)) => Some(*n),
256            _ => None,
257        })
258        .ok_or_else(|| vm.runtime_err("abs: expected an integer or a float"))?;
259    let result = int
260        .checked_abs()
261        .ok_or_else(|| vm.runtime_err("abs: integer overflow"))?;
262    alloc_int(vm, result)
263}
264
265/// `assert(condition: bool)` -- a `false` condition aborts the program.
266///
267/// a `true` condition is a no-op returning `void`; a `false` condition is a
268/// `Runtime` "assertion failed". a non-`bool` argument is a `Runtime` error.
269fn assert(vm: &mut Vm, args: &[Value]) -> Result<Value, QalaError> {
270    let arg = one_arg(vm, args, "assert")?;
271    let condition = arg
272        .as_bool()
273        .ok_or_else(|| vm.runtime_err("assert: expected a boolean"))?;
274    if condition {
275        Ok(Value::void())
276    } else {
277        Err(vm.runtime_err("assertion failed"))
278    }
279}
280
281/// `len(collection) -> i64` -- the length of an array, a tuple, or a string.
282///
283/// an array's / tuple's length is its element count; a string's length is its
284/// count of Unicode scalar values (`chars().count()`), matching the
285/// VM's `LEN` opcode. the result is a heap `Int`. any other value is a
286/// `Runtime` error.
287fn len(vm: &mut Vm, args: &[Value]) -> Result<Value, QalaError> {
288    let arg = one_arg(vm, args, "len")?;
289    let slot = arg
290        .as_pointer()
291        .ok_or_else(|| vm.runtime_err("len: expected an array or string"))?;
292    let length = match vm.heap.get(slot) {
293        Some(HeapObject::Array(items)) | Some(HeapObject::Tuple(items)) => items.len(),
294        Some(HeapObject::Str(s)) => s.chars().count(),
295        _ => return Err(vm.runtime_err("len: expected an array or string")),
296    };
297    alloc_int(vm, length as i64)
298}
299
300/// `push(array, value)` -- append `value` to a heap array in place.
301///
302/// the first argument is an array pointer, the second the value to append. the
303/// array is mutated through the heap. the typechecker types `push` as
304/// returning `void`, so this returns `void`. a non-array first argument is a
305/// `Runtime` error.
306fn push(vm: &mut Vm, args: &[Value]) -> Result<Value, QalaError> {
307    let (array, value) = match args {
308        [a, v] => (*a, *v),
309        _ => {
310            return Err(vm.runtime_err(&format!("push expects 2 arguments, got {}", args.len())));
311        }
312    };
313    let slot = array
314        .as_pointer()
315        .ok_or_else(|| vm.runtime_err("push: expected an array"))?;
316    match vm.heap.get_mut(slot) {
317        Some(HeapObject::Array(items)) => {
318            items.push(value);
319            Ok(Value::void())
320        }
321        _ => Err(vm.runtime_err("push: expected an array")),
322    }
323}
324
325/// `pop(array) -> Option<T>` -- remove and return the last element.
326///
327/// the argument is an array pointer. the array is mutated through the heap; the
328/// removed element is wrapped `Some(element)`, and an empty array yields
329/// `None`. a non-array argument is a `Runtime` error.
330fn pop(vm: &mut Vm, args: &[Value]) -> Result<Value, QalaError> {
331    let arg = one_arg(vm, args, "pop")?;
332    let slot = arg
333        .as_pointer()
334        .ok_or_else(|| vm.runtime_err("pop: expected an array"))?;
335    let removed = match vm.heap.get_mut(slot) {
336        Some(HeapObject::Array(items)) => items.pop(),
337        _ => return Err(vm.runtime_err("pop: expected an array")),
338    };
339    make_option(vm, removed)
340}
341
342/// `type_of(x) -> str` -- the runtime type name of `x` as a string.
343///
344/// the type name comes from [`Vm::runtime_type_name`] -- the same helper
345/// `get_state` uses to type-tint a value -- so `type_of` and the playground's
346/// type display always agree. the result is a heap `Str`: `"i64"`, `"f64"`,
347/// `"bool"`, `"str"`, `"byte"`, `"void"`, `"[i64]"`, `"(i64, str)"`, a struct's
348/// declared name, `"Enum::Variant"`, and so on.
349fn type_of(vm: &mut Vm, args: &[Value]) -> Result<Value, QalaError> {
350    let arg = one_arg(vm, args, "type_of")?;
351    let name = vm.runtime_type_name(arg);
352    alloc_str(vm, name)
353}
354
355/// `open(path: str) -> FileHandle` -- open a mock file handle.
356///
357/// the argument is the path string. the VM does no real file I/O (it runs in a
358/// WASM sandbox); `open` builds a [`HeapObject::FileHandle`] carrying the path,
359/// an empty mock content string, and `closed = false`. the mock content is
360/// always empty in v1 -- a real virtual filesystem is deferred. a non-string
361/// argument is a `Runtime` error.
362fn open(vm: &mut Vm, args: &[Value]) -> Result<Value, QalaError> {
363    let arg = one_arg(vm, args, "open")?;
364    let path_slot = arg
365        .as_pointer()
366        .ok_or_else(|| vm.runtime_err("open: expected a string path"))?;
367    let path = match vm.heap.get(path_slot) {
368        Some(HeapObject::Str(s)) => s.clone(),
369        _ => return Err(vm.runtime_err("open: expected a string path")),
370    };
371    let slot = vm
372        .heap
373        .alloc(HeapObject::FileHandle {
374            path,
375            content: String::new(),
376            closed: false,
377        })
378        .ok_or_else(|| vm.runtime_err("heap exhausted"))?;
379    Ok(Value::pointer(slot))
380}
381
382/// `close(handle: FileHandle)` -- mark a file handle closed.
383///
384/// the argument is a file-handle pointer; `close` flips its `closed` flag.
385/// closing an already-closed handle is a harmless no-op. returns `void`. a
386/// non-handle argument is a `Runtime` error.
387fn close(vm: &mut Vm, args: &[Value]) -> Result<Value, QalaError> {
388    let arg = one_arg(vm, args, "close")?;
389    let slot = arg
390        .as_pointer()
391        .ok_or_else(|| vm.runtime_err("close: expected a file handle"))?;
392    match vm.heap.get_mut(slot) {
393        Some(HeapObject::FileHandle { closed, .. }) => {
394            *closed = true;
395            Ok(Value::void())
396        }
397        _ => Err(vm.runtime_err("close: expected a file handle")),
398    }
399}
400
401/// `map(array, callback) -> array` -- apply `callback` to each element.
402///
403/// the first argument is an array pointer, the second a function value (a user
404/// callback). the elements are copied into a Rust-local `Vec` first, then each
405/// is passed to [`Vm::call_function_value`], which re-enters the VM's call
406/// machinery; the results collect into a fresh heap array. the per-element loop
407/// state -- the element list, the result vec -- lives in Rust locals, so a
408/// callback that itself calls `map` is re-entrant and does not corrupt VM
409/// state. a non-array first argument is a `Runtime` error.
410fn map(vm: &mut Vm, args: &[Value]) -> Result<Value, QalaError> {
411    let (array, callback) = match args {
412        [a, c] => (*a, *c),
413        _ => {
414            return Err(vm.runtime_err(&format!("map expects 2 arguments, got {}", args.len())));
415        }
416    };
417    let elements = heap_array_elements(vm, array, "map")?;
418    let mut results: Vec<Value> = Vec::with_capacity(elements.len());
419    for element in elements {
420        let mapped = vm.call_function_value(callback, &[element])?;
421        results.push(mapped);
422    }
423    alloc_array(vm, results)
424}
425
426/// `filter(array, callback) -> array` -- keep the elements `callback` accepts.
427///
428/// the callback returns a `bool`; an element is kept when the callback returns
429/// `true`. like [`map`], the loop state stays in Rust locals, so a nested
430/// `filter` (or `map`) inside the callback is re-entrant. a non-array first
431/// argument, or a callback that returns a non-`bool`, is a `Runtime` error.
432fn filter(vm: &mut Vm, args: &[Value]) -> Result<Value, QalaError> {
433    let (array, callback) = match args {
434        [a, c] => (*a, *c),
435        _ => {
436            return Err(vm.runtime_err(&format!("filter expects 2 arguments, got {}", args.len())));
437        }
438    };
439    let elements = heap_array_elements(vm, array, "filter")?;
440    let mut kept: Vec<Value> = Vec::new();
441    for element in elements {
442        let verdict = vm.call_function_value(callback, &[element])?;
443        let keep = verdict
444            .as_bool()
445            .ok_or_else(|| vm.runtime_err("filter: the callback must return a boolean"))?;
446        if keep {
447            kept.push(element);
448        }
449    }
450    alloc_array(vm, kept)
451}
452
453/// `reduce(array, initial, callback) -> value` -- fold an array.
454///
455/// the arguments are an array pointer, an initial accumulator value, and a
456/// callback. for each element the callback is called with `(accumulator,
457/// element)` and its result becomes the new accumulator; the final accumulator
458/// is returned. the accumulator lives in a Rust local, so a callback that
459/// itself folds is re-entrant. a non-array first argument is a `Runtime` error.
460fn reduce(vm: &mut Vm, args: &[Value]) -> Result<Value, QalaError> {
461    let (array, initial, callback) = match args {
462        [a, i, c] => (*a, *i, *c),
463        _ => {
464            return Err(vm.runtime_err(&format!("reduce expects 3 arguments, got {}", args.len())));
465        }
466    };
467    let elements = heap_array_elements(vm, array, "reduce")?;
468    let mut accumulator = initial;
469    for element in elements {
470        accumulator = vm.call_function_value(callback, &[accumulator, element])?;
471    }
472    Ok(accumulator)
473}
474
475/// `FileHandle.read_all(self) -> Result<str, str>` -- read a handle's content.
476///
477/// `read_all` is the only stdlib method in v1; codegen emits the receiver as
478/// argument 0, so `args[0]` is the file-handle pointer. the mock handle's
479/// content (empty in v1 -- see the module doc) is returned as `Ok(content)`.
480/// the `Result` lets a Qala program use `?` / `or` on the read, which is what
481/// `defer-demo.qala` exercises. a non-handle receiver is a `Runtime` error.
482fn read_all(vm: &mut Vm, args: &[Value]) -> Result<Value, QalaError> {
483    let arg = one_arg(vm, args, "read_all")?;
484    let slot = arg
485        .as_pointer()
486        .ok_or_else(|| vm.runtime_err("read_all: expected a file handle"))?;
487    let content = match vm.heap.get(slot) {
488        Some(HeapObject::FileHandle { content, .. }) => content.clone(),
489        _ => return Err(vm.runtime_err("read_all: expected a file handle")),
490    };
491    let payload = alloc_str(vm, content)?;
492    make_ok(vm, payload)
493}
494
495/// copy a heap array's elements into an owned `Vec`.
496///
497/// `map` / `filter` / `reduce` all need the element list as a Rust local before
498/// they re-enter the VM (so the iteration does not hold a borrow of the heap
499/// across a callback that may itself allocate). `value` must point at a
500/// [`HeapObject::Array`]; anything else is a `Runtime` error naming the calling
501/// function.
502fn heap_array_elements(vm: &Vm, value: Value, name: &str) -> Result<Vec<Value>, QalaError> {
503    let slot = value
504        .as_pointer()
505        .ok_or_else(|| vm.runtime_err(&format!("{name}: expected an array")))?;
506    match vm.heap.get(slot) {
507        Some(HeapObject::Array(items)) => Ok(items.clone()),
508        _ => Err(vm.runtime_err(&format!("{name}: expected an array"))),
509    }
510}
511
512#[cfg(test)]
513mod tests {
514    use super::*;
515    use crate::chunk::{Chunk, Program};
516    use crate::codegen::compile_program;
517    use crate::lexer::Lexer;
518    use crate::parser::Parser;
519    use crate::typechecker::check_program;
520
521    /// a VM over a single empty `main` chunk -- enough to exercise a stdlib
522    /// function that does not re-enter the VM (everything except
523    /// `map` / `filter` / `reduce`). the native functions take `&mut Vm` for
524    /// the heap and the console, not for a running frame, so an empty `main`
525    /// is a sufficient fixture.
526    fn bare_vm() -> Vm {
527        let mut chunk = Chunk::new();
528        chunk.write_op(crate::opcode::Opcode::Return, 1);
529        let mut program = Program::new();
530        program.chunks.push(chunk);
531        program.fn_names.push("main".to_string());
532        program.main_index = 0;
533        Vm::new(program, String::new())
534    }
535
536    /// compile a Qala program through the full pipeline. used to build a VM
537    /// whose `Program` carries real callback chunks for the `map` / `filter` /
538    /// `reduce` tests. panics on any pipeline error -- the fixtures are all
539    /// known-good source.
540    fn compile(src: &str) -> Program {
541        let tokens = Lexer::tokenize(src).expect("lex");
542        let ast = Parser::parse(&tokens).expect("parse");
543        let (typed, errors, _) = check_program(&ast, src);
544        assert!(errors.is_empty(), "typecheck errors: {errors:?}");
545        compile_program(&typed, src).expect("codegen")
546    }
547
548    /// the fn-id of the user function named `name` in `program`.
549    fn fn_id(program: &Program, name: &str) -> u16 {
550        program
551            .fn_names
552            .iter()
553            .position(|n| n == name)
554            .unwrap_or_else(|| panic!("no function named {name}")) as u16
555    }
556
557    /// allocate a heap `Int` in `vm` and return the pointer value.
558    fn int(vm: &mut Vm, n: i64) -> Value {
559        alloc_int(vm, n).expect("alloc int")
560    }
561
562    /// allocate a heap `Str` in `vm` and return the pointer value.
563    fn string(vm: &mut Vm, s: &str) -> Value {
564        alloc_str(vm, s.to_string()).expect("alloc str")
565    }
566
567    /// the `i64` a result value decodes to -- a pointer to a heap `Int`.
568    fn result_i64(vm: &Vm, value: Value) -> i64 {
569        let slot = value.as_pointer().expect("a heap pointer");
570        match vm.heap.get(slot) {
571            Some(HeapObject::Int(n)) => *n,
572            _ => panic!("the value does not point at a heap Int"),
573        }
574    }
575
576    /// the `str` a result value decodes to -- a pointer to a heap `Str`.
577    fn result_str(vm: &Vm, value: Value) -> String {
578        let slot = value.as_pointer().expect("a heap pointer");
579        match vm.heap.get(slot) {
580            Some(HeapObject::Str(s)) => s.clone(),
581            _ => panic!("the value does not point at a heap Str"),
582        }
583    }
584
585    /// the `Vec<Value>` a result value decodes to -- a pointer to a heap array.
586    fn result_array(vm: &Vm, value: Value) -> Vec<Value> {
587        let slot = value.as_pointer().expect("a heap pointer");
588        match vm.heap.get(slot) {
589            Some(HeapObject::Array(items)) => items.clone(),
590            _ => panic!("the value does not point at a heap array"),
591        }
592    }
593
594    /// the rendered form of a finished program's result.
595    ///
596    /// after `run()`, a program's `RETURN` from `main` leaves the result on the
597    /// value stack; `get_state` renders the stack, so the last entry's
598    /// `rendered` string is the result -- `"55"` for an `i64` 55, and so on.
599    /// reading it through the public `get_state` avoids reaching into the VM's
600    /// private value stack from this module.
601    fn program_result(vm: &Vm) -> String {
602        vm.get_state()
603            .stack
604            .last()
605            .expect("a finished program left a result value")
606            .rendered
607            .clone()
608    }
609
610    #[test]
611    fn len_of_an_array_and_of_a_string() {
612        let mut vm = bare_vm();
613        // an array of three i64 elements.
614        let a = int(&mut vm, 10);
615        let b = int(&mut vm, 20);
616        let c = int(&mut vm, 30);
617        let array = alloc_array(&mut vm, vec![a, b, c]).expect("alloc array");
618        let array_len = len(&mut vm, &[array]).expect("len of an array");
619        assert_eq!(result_i64(&vm, array_len), 3, "an array of 3 has len 3");
620        // a five-character string.
621        let s = string(&mut vm, "hello");
622        let str_len = len(&mut vm, &[s]).expect("len of a string");
623        assert_eq!(result_i64(&vm, str_len), 5, "\"hello\" has len 5");
624    }
625
626    #[test]
627    fn push_appends_and_pop_returns_some_then_none() {
628        let mut vm = bare_vm();
629        let array = alloc_array(&mut vm, Vec::new()).expect("alloc array");
630        // push two values.
631        let one = int(&mut vm, 1);
632        let two = int(&mut vm, 2);
633        push(&mut vm, &[array, one]).expect("push 1");
634        push(&mut vm, &[array, two]).expect("push 2");
635        assert_eq!(result_array(&vm, array).len(), 2, "two pushes -> length 2");
636        // pop returns Some(last).
637        let popped = pop(&mut vm, &[array]).expect("pop");
638        let popped_slot = popped.as_pointer().expect("Some is a heap pointer");
639        match vm.heap.get(popped_slot) {
640            Some(HeapObject::EnumVariant {
641                variant, payload, ..
642            }) => {
643                assert_eq!(variant, "Some", "a non-empty pop returns Some");
644                assert_eq!(result_i64(&vm, payload[0]), 2, "the last element popped");
645            }
646            _ => panic!("pop must return an Option enum variant"),
647        }
648        // pop the remaining element, then pop an empty array -> None.
649        pop(&mut vm, &[array]).expect("pop the last element");
650        let none = pop(&mut vm, &[array]).expect("pop an empty array");
651        let none_slot = none.as_pointer().expect("None is a heap pointer");
652        match vm.heap.get(none_slot) {
653            Some(HeapObject::EnumVariant { variant, .. }) => {
654                assert_eq!(variant, "None", "an empty pop returns None");
655            }
656            _ => panic!("pop of an empty array must return None"),
657        }
658    }
659
660    #[test]
661    fn sqrt_of_four_is_two() {
662        let mut vm = bare_vm();
663        let result = sqrt(&mut vm, &[Value::from_f64(4.0)]).expect("sqrt");
664        assert_eq!(result.as_f64(), Some(2.0), "sqrt(4.0) == 2.0");
665    }
666
667    #[test]
668    fn abs_of_a_negative_int_and_a_negative_float() {
669        let mut vm = bare_vm();
670        // abs of an i64 returns a heap i64.
671        let neg_three = int(&mut vm, -3);
672        let abs_int = abs(&mut vm, &[neg_three]).expect("abs of an int");
673        assert_eq!(result_i64(&vm, abs_int), 3, "abs(-3) == 3");
674        // abs of an f64 returns an f64.
675        let abs_float = abs(&mut vm, &[Value::from_f64(-1.5)]).expect("abs of a float");
676        assert_eq!(abs_float.as_f64(), Some(1.5), "abs(-1.5) == 1.5");
677    }
678
679    #[test]
680    fn assert_true_is_a_no_op_and_assert_false_is_a_runtime_error() {
681        let mut vm = bare_vm();
682        // assert(true) returns void without error.
683        let ok = assert(&mut vm, &[Value::bool(true)]).expect("assert(true)");
684        assert!(ok.as_void(), "assert(true) returns void");
685        // assert(false) is a Runtime "assertion failed".
686        match assert(&mut vm, &[Value::bool(false)]) {
687            Err(QalaError::Runtime { message, .. }) => {
688                assert!(message.contains("assertion failed"), "got: {message}");
689            }
690            Err(other) => panic!("expected an assertion-failed Runtime error, got {other:?}"),
691            Ok(_) => panic!("assert(false) must error"),
692        }
693    }
694
695    #[test]
696    fn type_of_returns_the_runtime_type_name_for_each_kind() {
697        let mut vm = bare_vm();
698        // a representative value of each primitive kind.
699        let i64_value = int(&mut vm, 7);
700        let str_value = string(&mut vm, "hi");
701        // an array of i64 -> "[i64]".
702        let e0 = int(&mut vm, 1);
703        let e1 = int(&mut vm, 2);
704        let i64_array = alloc_array(&mut vm, vec![e0, e1]).expect("alloc array");
705        // a struct and an enum variant, built directly on the heap.
706        let shape_struct = vm
707            .heap
708            .alloc(HeapObject::Struct {
709                type_name: "Shape".to_string(),
710                fields: Vec::new(),
711            })
712            .expect("alloc struct");
713        let shape_variant = vm
714            .heap
715            .alloc(HeapObject::EnumVariant {
716                type_name: "Shape".to_string(),
717                variant: "Circle".to_string(),
718                payload: Vec::new(),
719            })
720            .expect("alloc variant");
721        let cases: Vec<(Value, &str)> = vec![
722            (i64_value, "i64"),
723            (Value::from_f64(1.5), "f64"),
724            (Value::bool(true), "bool"),
725            (str_value, "str"),
726            (Value::byte(65), "byte"),
727            (Value::void(), "void"),
728            (i64_array, "[i64]"),
729            (Value::pointer(shape_struct), "Shape"),
730            (Value::pointer(shape_variant), "Shape::Circle"),
731        ];
732        for (value, expected) in cases {
733            let result = type_of(&mut vm, &[value]).expect("type_of");
734            assert_eq!(result_str(&vm, result), expected, "type_of mismatch");
735        }
736    }
737
738    #[test]
739    fn print_and_println_append_to_the_console() {
740        let mut vm = bare_vm();
741        // println pushes one console line with a trailing newline.
742        let line = string(&mut vm, "first line");
743        println(&mut vm, &[line]).expect("println");
744        assert_eq!(vm.console, vec!["first line\n".to_string()]);
745        // print pushes its own console entry without a trailing newline.
746        let a = string(&mut vm, "a");
747        let b = string(&mut vm, "b");
748        print(&mut vm, &[a]).expect("print a");
749        print(&mut vm, &[b]).expect("print b");
750        assert_eq!(
751            vm.console,
752            vec!["first line\n".to_string(), "a".to_string(), "b".to_string(),],
753            "println entries end with newline; print entries do not"
754        );
755    }
756
757    #[test]
758    fn println_adds_newline_print_does_not() {
759        // joining all console entries produces correct newline separation:
760        // two println calls each add a '\n'; a print call does not.
761        let mut vm = bare_vm();
762        let a = string(&mut vm, "a");
763        let b = string(&mut vm, "b");
764        let c = string(&mut vm, "c");
765        println(&mut vm, &[a]).expect("println a");
766        println(&mut vm, &[b]).expect("println b");
767        print(&mut vm, &[c]).expect("print c");
768        let joined: String = vm.console.concat();
769        assert_eq!(
770            joined, "a\nb\nc",
771            "two println then print joined is a\\nb\\nc"
772        );
773    }
774
775    #[test]
776    fn open_returns_a_file_handle_and_close_marks_it_closed() {
777        let mut vm = bare_vm();
778        let path = string(&mut vm, "data.txt");
779        let handle = open(&mut vm, &[path]).expect("open");
780        let slot = handle.as_pointer().expect("open returns a heap pointer");
781        // the freshly opened handle is not closed and carries the path.
782        match vm.heap.get(slot) {
783            Some(HeapObject::FileHandle { path, closed, .. }) => {
784                assert_eq!(path, "data.txt", "the handle carries the path");
785                assert!(!closed, "a freshly opened handle is open");
786            }
787            _ => panic!("open must return a FileHandle"),
788        }
789        // close flips the flag.
790        let void = close(&mut vm, &[handle]).expect("close");
791        assert!(void.as_void(), "close returns void");
792        match vm.heap.get(slot) {
793            Some(HeapObject::FileHandle { closed, .. }) => {
794                assert!(closed, "close marks the handle closed");
795            }
796            _ => panic!("the handle is still a FileHandle after close"),
797        }
798    }
799
800    #[test]
801    fn read_all_returns_ok_with_the_handle_content() {
802        let mut vm = bare_vm();
803        let path = string(&mut vm, "data.txt");
804        let handle = open(&mut vm, &[path]).expect("open");
805        // read_all of the mock handle returns Ok(content) -- the v1 mock
806        // content is the empty string.
807        let result = read_all(&mut vm, &[handle]).expect("read_all");
808        let slot = result.as_pointer().expect("Ok is a heap pointer");
809        match vm.heap.get(slot) {
810            Some(HeapObject::EnumVariant {
811                type_name,
812                variant,
813                payload,
814            }) => {
815                assert_eq!(type_name, "Result", "read_all returns a Result");
816                assert_eq!(variant, "Ok", "the mock read succeeds");
817                assert_eq!(
818                    result_str(&vm, payload[0]),
819                    "",
820                    "the v1 mock content is empty"
821                );
822            }
823            _ => panic!("read_all must return a Result enum variant"),
824        }
825    }
826
827    #[test]
828    fn map_applies_a_user_callback_to_each_element() {
829        // a program whose `double` user function is the map callback.
830        let src = "fn double(x: i64) -> i64 is pure { return x * 2 }\n\
831                   fn main() is io {}\n";
832        let program = compile(src);
833        let double = fn_id(&program, "double");
834        let mut vm = Vm::new(program, src.to_string());
835        let e0 = int(&mut vm, 1);
836        let e1 = int(&mut vm, 2);
837        let e2 = int(&mut vm, 3);
838        let array = alloc_array(&mut vm, vec![e0, e1, e2]).expect("alloc array");
839        let mapped = map(&mut vm, &[array, Value::function(double)]).expect("map");
840        let items = result_array(&vm, mapped);
841        let values: Vec<i64> = items.iter().map(|v| result_i64(&vm, *v)).collect();
842        assert_eq!(values, vec![2, 4, 6], "map(double) doubles every element");
843    }
844
845    #[test]
846    fn filter_keeps_the_elements_the_callback_accepts() {
847        // `is_even` is the filter predicate.
848        let src = "fn is_even(x: i64) -> bool is pure { return x % 2 == 0 }\n\
849                   fn main() is io {}\n";
850        let program = compile(src);
851        let is_even = fn_id(&program, "is_even");
852        let mut vm = Vm::new(program, src.to_string());
853        let mut elements = Vec::new();
854        for n in 1..=6 {
855            elements.push(int(&mut vm, n));
856        }
857        let array = alloc_array(&mut vm, elements).expect("alloc array");
858        let filtered = filter(&mut vm, &[array, Value::function(is_even)]).expect("filter");
859        let items = result_array(&vm, filtered);
860        let values: Vec<i64> = items.iter().map(|v| result_i64(&vm, *v)).collect();
861        assert_eq!(values, vec![2, 4, 6], "filter(is_even) keeps the evens");
862    }
863
864    #[test]
865    fn reduce_folds_an_array_with_the_callback() {
866        // `add` is the fold callback; reduce sums 1..=4 from an initial 0.
867        let src = "fn add(a: i64, b: i64) -> i64 is pure { return a + b }\n\
868                   fn main() is io {}\n";
869        let program = compile(src);
870        let add = fn_id(&program, "add");
871        let mut vm = Vm::new(program, src.to_string());
872        let mut elements = Vec::new();
873        for n in 1..=4 {
874            elements.push(int(&mut vm, n));
875        }
876        let array = alloc_array(&mut vm, elements).expect("alloc array");
877        let initial = int(&mut vm, 0);
878        let total = reduce(&mut vm, &[array, initial, Value::function(add)]).expect("reduce");
879        assert_eq!(result_i64(&vm, total), 10, "reduce(+) of 1..=4 is 10");
880    }
881
882    #[test]
883    fn dispatch_routes_each_fn_id_and_rejects_an_unknown_one() {
884        let mut vm = bare_vm();
885        // fn-id 40002 is `sqrt`: dispatch must route there.
886        let via_dispatch = dispatch(&mut vm, 40002, &[Value::from_f64(9.0)]).expect("sqrt");
887        assert_eq!(via_dispatch.as_f64(), Some(3.0), "dispatch(40002) is sqrt");
888        // an fn-id above the table is malformed bytecode -> a clean error.
889        match dispatch(&mut vm, 49999, &[]) {
890            Err(QalaError::Runtime { message, .. }) => {
891                assert!(
892                    message.contains("unknown stdlib function"),
893                    "got: {message}"
894                );
895            }
896            Err(other) => panic!("expected an unknown-stdlib Runtime error, got {other:?}"),
897            Ok(_) => panic!("an unknown fn-id must error"),
898        }
899        // an fn-id below the stdlib base must not reach dispatch cleanly.
900        match dispatch(&mut vm, 5, &[]) {
901            Err(QalaError::Runtime { message, .. }) => {
902                assert!(message.contains("not a stdlib id"), "got: {message}");
903            }
904            Err(other) => panic!("expected an invariant Runtime error, got {other:?}"),
905            Ok(_) => panic!("a user fn-id must not dispatch as stdlib"),
906        }
907    }
908
909    #[test]
910    fn a_wrong_argument_count_is_a_runtime_error_not_a_panic() {
911        let mut vm = bare_vm();
912        // sqrt expects one argument; zero is malformed bytecode.
913        match sqrt(&mut vm, &[]) {
914            Err(QalaError::Runtime { message, .. }) => {
915                assert!(message.contains("expects 1 argument"), "got: {message}");
916            }
917            Err(other) => panic!("expected an arity Runtime error, got {other:?}"),
918            Ok(_) => panic!("sqrt with no argument must error"),
919        }
920        // a wrong-type argument is likewise a clean error, never a panic.
921        match len(&mut vm, &[Value::bool(true)]) {
922            Err(QalaError::Runtime { message, .. }) => {
923                assert!(
924                    message.contains("expected an array or string"),
925                    "got: {message}"
926                );
927            }
928            Err(other) => panic!("expected a wrong-type Runtime error, got {other:?}"),
929            Ok(_) => panic!("len of a bool must error"),
930        }
931    }
932
933    // ---- re-entrancy + the cross-cutting smoke tests ----
934
935    #[test]
936    fn map_reentrant_a_nested_map_inside_the_callback_works() {
937        // RESEARCH Pitfall 7: a `map` whose callback itself calls `map` must
938        // produce the correct nested result. `double_row` is the outer
939        // callback; it calls `map(row, double)` -- a nested re-entry. the loop
940        // state of each `map` stays in Rust locals, so the inner map does not
941        // corrupt the outer. the grid [[1,2],[3,4]] doubled is [[2,4],[6,8]];
942        // the program returns doubled[1][1], which must be 8.
943        let src = "fn double(x: i64) -> i64 is pure { return x * 2 }\n\
944                   fn double_row(row: [i64]) -> [i64] is pure {\n    \
945                   return map(row, double)\n}\n\
946                   fn main() -> i64 is pure {\n    \
947                   let grid = [[1, 2], [3, 4]]\n    \
948                   let doubled = map(grid, double_row)\n    \
949                   return doubled[1][1]\n}\n";
950        let program = compile(src);
951        let mut vm = Vm::new(program, src.to_string());
952        vm.run().expect("a nested-map program runs clean");
953        assert_eq!(
954            program_result(&vm),
955            "8",
956            "the nested map doubled [3,4] to [6,8]; [1][1] is 8"
957        );
958    }
959
960    #[test]
961    fn fibonacci_smoke_computes_the_correct_numeric_result() {
962        // the success-criterion smoke test: a recursive fib compiled and run
963        // end to end. fib(10) is 55.
964        let src = "fn fib(n: i64) -> i64 is pure {\n    \
965                   if n <= 1 { return n }\n    \
966                   return fib(n - 1) + fib(n - 2)\n}\n\
967                   fn main() -> i64 is pure {\n    return fib(10)\n}\n";
968        let program = compile(src);
969        let mut vm = Vm::new(program, src.to_string());
970        vm.run().expect("the fibonacci program runs clean");
971        assert_eq!(program_result(&vm), "55", "fib(10) must be 55");
972    }
973
974    /// read a bundled example from `playground/public/examples/`.
975    fn example_source(name: &str) -> String {
976        let path = format!(
977            "{}/../../playground/public/examples/{name}.qala",
978            env!("CARGO_MANIFEST_DIR"),
979        );
980        std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("read {path}: {e}"))
981    }
982
983    /// compile and run a bundled example, returning the finished VM so the
984    /// caller can inspect its console.
985    fn run_example(name: &str) -> Vm {
986        let src = example_source(name);
987        let program = compile(&src);
988        let mut vm = Vm::new(program, src);
989        vm.run()
990            .unwrap_or_else(|e| panic!("{name}.qala did not run to completion: {e:?}"));
991        vm
992    }
993
994    #[test]
995    fn six_examples_run_to_completion() {
996        // every bundled example compiles, runs to completion without a runtime
997        // error, and -- for the examples that print -- produces the expected
998        // console output. the examples are the acceptance corpus for Phase 5.
999
1000        // hello: string interpolation -> "hello, world!".
1001        let hello = run_example("hello");
1002        assert!(
1003            hello
1004                .console
1005                .iter()
1006                .any(|l| l.trim_end_matches('\n') == "hello, world!"),
1007            "hello.qala console: {:?}",
1008            hello.console,
1009        );
1010
1011        // fibonacci: a for loop printing fib(0..14). the console is the 15
1012        // lines fib(0)=0 .. fib(14)=377.
1013        let fibonacci = run_example("fibonacci");
1014        assert!(
1015            fibonacci
1016                .console
1017                .iter()
1018                .any(|l| l.trim_end_matches('\n') == "fib(0) = 0"),
1019            "fibonacci.qala console: {:?}",
1020            fibonacci.console,
1021        );
1022        assert!(
1023            fibonacci
1024                .console
1025                .iter()
1026                .any(|l| l.trim_end_matches('\n') == "fib(10) = 55"),
1027            "fibonacci.qala must print fib(10) = 55, console: {:?}",
1028            fibonacci.console,
1029        );
1030        assert!(
1031            fibonacci
1032                .console
1033                .iter()
1034                .any(|l| l.trim_end_matches('\n') == "fib(14) = 377"),
1035            "fibonacci.qala must print fib(14) = 377, console: {:?}",
1036            fibonacci.console,
1037        );
1038
1039        // effects: a pure function called from an io function -> "7 squared: 49".
1040        let effects = run_example("effects");
1041        assert!(
1042            effects
1043                .console
1044                .iter()
1045                .any(|l| l.trim_end_matches('\n') == "7 squared: 49"),
1046            "effects.qala console: {:?}",
1047            effects.console,
1048        );
1049
1050        // pattern-matching: enums with data + guards. it prints the three
1051        // shape areas then the three classify results.
1052        let pattern_matching = run_example("pattern-matching");
1053        assert!(
1054            !pattern_matching.console.is_empty(),
1055            "pattern-matching.qala must print something",
1056        );
1057        assert!(
1058            pattern_matching
1059                .console
1060                .iter()
1061                .any(|l| l.trim_end_matches('\n') == "positive"),
1062            "pattern-matching.qala classify(42) must print positive, console: {:?}",
1063            pattern_matching.console,
1064        );
1065        assert!(
1066            pattern_matching
1067                .console
1068                .iter()
1069                .any(|l| l.trim_end_matches('\n') == "negative"),
1070            "pattern-matching.qala classify(-7) must print negative, console: {:?}",
1071            pattern_matching.console,
1072        );
1073        assert!(
1074            pattern_matching
1075                .console
1076                .iter()
1077                .any(|l| l.trim_end_matches('\n') == "zero"),
1078            "pattern-matching.qala classify(0) must print zero, console: {:?}",
1079            pattern_matching.console,
1080        );
1081
1082        // pipeline: |> with map / filter. 5 |> double |> add_one |> double is
1083        // ((5*2)+1)*2 = 22; the even numbers 1..10 filtered then doubled are
1084        // 4, 8, 12, 16, 20.
1085        let pipeline = run_example("pipeline");
1086        assert!(
1087            pipeline.console.iter().any(|l| l.contains("= 22")),
1088            "pipeline.qala must print the pipeline result 22, console: {:?}",
1089            pipeline.console,
1090        );
1091        assert!(
1092            pipeline
1093                .console
1094                .iter()
1095                .any(|l| l.trim_end_matches('\n') == "20"),
1096            "pipeline.qala filter+map must print the doubled even 20, console: {:?}",
1097            pipeline.console,
1098        );
1099
1100        // defer-demo: a resource with defer close. process_file's mock read
1101        // returns empty content, so len(content) == 0 takes the Err path; the
1102        // `or "no data"` fallback supplies the value and main prints it.
1103        let defer_demo = run_example("defer-demo");
1104        assert!(
1105            defer_demo.console.iter().any(|l| l.starts_with("got: ")),
1106            "defer-demo.qala must print a got: line, console: {:?}",
1107            defer_demo.console,
1108        );
1109        // the defer closed the handle on every exit path -- no leak.
1110        assert!(
1111            defer_demo.leak_log.is_empty(),
1112            "defer-demo.qala closes its handle via defer -- no leak, got: {:?}",
1113            defer_demo.leak_log,
1114        );
1115    }
1116}