cas_compiler/
lib.rs

1#![doc = include_str!("../README.md")]
2
3pub mod expr;
4pub mod error;
5pub mod instruction;
6pub mod item;
7pub mod sym_table;
8
9use cas_compute::{consts::all as all_consts, funcs::all as all_funcs};
10use cas_error::Error;
11use cas_parser::parser::ast::{FuncHeader, LitSym, Stmt};
12use error::{
13    OverrideBuiltinConstant,
14    OverrideBuiltinFunction,
15    UnknownVariable,
16};
17use std::collections::{HashMap, HashSet};
18use expr::compile_stmts;
19pub use instruction::{Instruction, InstructionKind};
20use item::{FuncDecl, Item, Symbol, SymbolDecl};
21use std::ops::Range;
22use sym_table::{Scope, SymbolTable};
23
24/// A label that can be used to reference a specific instruction in the bytecode.
25///
26/// The internal value is simply a unique ID that is resolved to the actual instruction during
27/// execution.
28#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
29pub struct Label(usize);
30
31/// The state of the compiler.
32#[derive(Clone, Debug, Default)]
33pub struct CompilerState {
34    /// The label pointing to the start of the current loop.
35    pub loop_start: Option<Label>,
36
37    /// The label pointing to the end of the current loop.
38    pub loop_end: Option<Label>,
39
40    /// Whether the current statement is the last statement in a block or the program, indicating
41    /// that its return value is the return value of the block / program.
42    pub last_stmt: bool,
43
44    /// Whether the expression being compiled is a top-level assignment expression, indicating that
45    /// its return value is not used.
46    ///
47    /// This is used to determine whether to use the [`InstructionKind::StoreVar`] or
48    /// [`InstructionKind::AssignVar`] instruction.
49    ///
50    /// For example, in the following code:
51    ///
52    /// ```calcscript
53    /// x = y = 2
54    /// ```
55    ///
56    /// The `x = ...` expression is a top-level assignment expression, so its return value is not
57    /// used within the same statement. However, the `y = 2` expression is not a top-level
58    /// assignment, as its return value is then passed to the `x = ...` expression. In this case,
59    /// the compiler will generate a [`InstructionKind::AssignVar`] instruction for `x` and a
60    /// [`InstructionKind::StoreVar`] instruction for `y`.
61    // TODO: this is not implemented yet
62    pub top_level_assign: bool,
63}
64
65/// A chunk containing a function definition.
66#[derive(Clone, Debug, Default)]
67pub struct Chunk {
68    /// The instructions in this chunk.
69    pub instructions: Vec<Instruction>,
70
71    /// The number of arguments the function takes.
72    pub arity: usize,
73}
74
75impl Chunk {
76    /// Creates a new chunk with the given arity.
77    pub fn new(arity: usize) -> Self {
78        Self {
79            instructions: Vec::new(),
80            arity,
81        }
82    }
83}
84
85/// Value returned by [`Compiler::new_chunk`].
86pub struct NewChunk {
87    /// The unique identifier for the function.
88    pub id: usize,
89
90    /// The index of the new chunk.
91    pub chunk: usize,
92
93    /// The symbols captured by the function from parent scopes.
94    pub captures: HashSet<usize>,
95}
96
97/// Returns the corresponding error if a symbol matches the name of a builtin symbol or function.
98fn check_override_builtin(symbol: &LitSym) -> Result<(), Error> {
99    if all_consts().contains(&*symbol.name) {
100        return Err(Error::new(vec![symbol.span.clone()], OverrideBuiltinConstant {
101            name: symbol.name.to_string(),
102        }));
103    }
104    if all_funcs().contains_key(&*symbol.name) {
105        return Err(Error::new(vec![symbol.span.clone()], OverrideBuiltinFunction {
106            name: symbol.name.to_string(),
107        }));
108    }
109    Ok(())
110}
111
112/// Returns the builtin constant or function with the given name, if it exists.
113fn resolve_builtin(symbol: &LitSym) -> Option<Symbol> {
114    all_consts()
115        .get(&*symbol.name)
116        .map(|name| Symbol::Builtin(name))
117        .or_else(|| {
118            all_funcs()
119                .get(&*symbol.name)
120                .map(|func| Symbol::Builtin(func.name()))
121        })
122}
123
124/// A compiler that provides tools to generate bytecode instructions for `cas-rs`'s virtual machine
125/// (see [`Vm`]).
126///
127/// **Note: If you're looking to run a CalcScript program, you should use the [`Vm`] struct found
128/// in `cas-vm` instead.**
129///
130/// This is the main entry point to the compiler. The compiler translates a CalcScript AST
131/// (produced by [`cas_parser`]) into a series of bytecode [`Chunk`]s, which can then be executed
132/// by `cas-rs`'s [`Vm`]. It also is mostly responsible for managing CalcScript's semantics through
133/// lexical scoping and symbol resolution, value stack layout, and generation of chunks. These
134/// details are described later in this documentation, but are not important if you're just looking
135/// to run a program.
136///
137/// To compile a complete program, it is recommended that you use [`Compiler::compile_program`],
138/// which is the easiest way to ensure the resulting bytecode is valid. However, there are also a
139/// number of other methods that can be used to manually compile CalcScript. There is one important
140/// rule to keep in mind when taking this approach (which would otherwise be handled by the
141/// compiler), which requires a quick explanation of [`Vm`]'s value stack.
142///
143/// During execution, [`Vm`] uses a value stack to keep track of values generated by and used
144/// around the bytecode. The compiler must ensure that the instructions it generates manipulates
145/// the value stack's semantics correctly.
146///
147/// The most important rule is that **the value stack must have exactly one value on it when the
148/// program finishes executing**. This is the value that is returned by the program (printed when
149/// using the `cas-rs` REPL). When manually compiling a program, you must ensure that each
150/// statement's instructions leave no value on the stack when the statement completes, except for
151/// the last statement in a block or chunk.
152///
153/// (Note that failing to uphold this rule will never result in undefined behavior; it will most
154/// likely either panic or result in an error during execution.)
155///
156/// Most CalcScript programs consist of a sequence of statements, for example:
157///
158/// ```calcscript
159/// x = 3
160/// y = 4
161/// z = hypot(x, y)
162/// ```
163///
164/// In this case, the compiler generates these instructions:
165///
166/// ```rust
167/// use cas_compiler::Compiler;
168/// use cas_parser::parser::Parser;
169///
170/// let ast = Parser::new("x = 3
171/// y = 4
172/// z = hypot(x, y)").try_parse_full_many().unwrap();
173///
174/// let compiler = Compiler::compile_program(ast).unwrap();
175///
176/// use cas_compiler::{item::Symbol, Instruction, InstructionKind::*};
177/// assert_eq!(compiler.chunks[0].instructions, vec![
178///     // x = 3
179///     Instruction { kind: LoadConst(3.into()), spans: vec![] },
180///     Instruction { kind: StoreVar(0), spans: vec![] },
181///     Instruction { kind: Drop, spans: vec![] },
182///
183///     // x = 4
184///     Instruction { kind: LoadConst(4.into()), spans: vec![] },
185///     Instruction { kind: StoreVar(1), spans: vec![] },
186///     Instruction { kind: Drop, spans: vec![] },
187///
188///     // z = hypot(x, y)
189///     Instruction { kind: LoadVar(Symbol::User(0)), spans: vec![22..23] },
190///     Instruction { kind: LoadVar(Symbol::User(1)), spans: vec![25..26] },
191///     Instruction { kind: LoadVar(Symbol::Builtin("hypot")), spans: vec![16..21] },
192///     Instruction { kind: Call(2), spans: vec![16..22, 26..27, 22..23, 25..26] },
193///     Instruction { kind: StoreVar(2), spans: vec![] }
194/// ]);
195/// ```
196///
197/// Notice that each statement is terminated by a [`InstructionKind::Drop`] instruction, except for
198/// the last one. For example, the first statement, `x = 3`, loads the constant `3` onto the stack.
199/// The [`InstructionKind::StoreVar`] instruction stores the value into the variable `x` (0), but
200/// does not remove it from the stack. The `Drop` instruction then removes the value from the
201/// stack, leaving the stack empty. (It is more optimal to use [`InstructionKind::AssignVar`] in
202/// this case, but the compiler does not implement this behavior yet.)
203///
204/// The final statement, `z = hypot(x, y)`, stores the computed value into the variable `z` (2),
205/// but does not drop the value from the stack, making it the final value on the stack, and thus
206/// the return value of the program.
207///
208/// You need to be mindful of this behavior when manually compiling programs.
209///
210/// [`Vm`]: https://docs.rs/cas-vm/latest/cas_vm/vm/struct.Vm.html.
211#[derive(Clone, Debug)]
212pub struct Compiler {
213    /// The bytecode chunks generated by the compiler.
214    ///
215    /// The entire program is represented as multiple chunks of bytecode, where each chunk
216    /// represents a function body. The first chunk represents the implicit "main" function.
217    pub chunks: Vec<Chunk>,
218
219    /// Labels generated by the compiler, mapped to the index of the instruction they reference.
220    ///
221    /// When created, labels aren't associated with any instruction. Before the bytecode is
222    /// executed, the compiler will resolve these labels to the actual instruction indices.
223    pub labels: HashMap<Label, Option<(usize, usize)>>,
224
225    /// A symbol table that maps identifiers to information about the values they represent.
226    ///
227    /// This is used to store information about variables and functions that are defined in the
228    /// program.
229    pub sym_table: SymbolTable,
230
231    /// Index of the current chunk.
232    ///
233    /// This value is manually updated by the compiler.
234    chunk: usize,
235
236    /// Next unique identifier for a symbol or function.
237    next_item_id: usize,
238
239    /// Holds state for the current loop.
240    state: CompilerState,
241}
242
243impl Default for Compiler {
244    fn default() -> Self {
245        Self {
246            chunks: vec![Chunk::default()], // add main chunk
247            labels: Default::default(),
248            sym_table: Default::default(),
249            chunk: 0,
250            next_item_id: 0,
251            state: Default::default(),
252        }
253    }
254}
255
256impl Compiler {
257    /// Creates a new compiler.
258    pub fn new() -> Self {
259        Self::default()
260    }
261
262    /// Compiles the given type into a sequence of [`Instruction`]s.
263    pub fn compile<T: Compile>(expr: T) -> Result<Self, Error> {
264        let mut compiler = Self::new();
265        expr.compile(&mut compiler)?;
266        Ok(compiler)
267    }
268
269    /// Compiles multiple statements into a sequence of [`Instruction`]s.
270    pub fn compile_program(stmts: Vec<Stmt>) -> Result<Self, Error> {
271        let mut compiler = Self::new();
272        compile_stmts(&stmts, &mut compiler)?;
273        Ok(compiler)
274    }
275
276    /// Creates a new compilation scope with the given modified state. Compilation that occurs in
277    /// this scope will then use the modified state.
278    pub fn with_state<F, G>(&mut self, modify_state: F, compile: G) -> Result<(), Error>
279    where
280        F: FnOnce(&mut CompilerState),
281        G: FnOnce(&mut Self) -> Result<(), Error>,
282    {
283        let old_state = self.state.clone();
284        modify_state(&mut self.state);
285        compile(&mut *self)?;
286        self.state = old_state;
287        Ok(())
288    }
289
290    /// Returns an immutable reference to the current chunk.
291    pub fn chunk(&self) -> &Chunk {
292        self.chunks.get(self.chunk).unwrap()
293    }
294
295    /// Returns a mutable reference to the current chunk.
296    pub fn chunk_mut(&mut self) -> &mut Chunk {
297        self.chunks.get_mut(self.chunk).unwrap()
298    }
299
300    /// Add an item to the symbol table at the current scope.
301    ///
302    /// If the item to add matches that of a builtin item, one of the following will occur:
303    ///
304    /// - If this function is called from the global scope, an [`OverrideBuiltinConstant`] or
305    ///   [`OverrideBuiltinFunction`] error is returned.
306    /// - If this function is called anywhere else, the symbol table will successfully be updated
307    ///   with the new item. This item shadows the existing builtin, meaning the builtin will not
308    ///   be accessible until the scope in which this item was declared, ends.
309    pub fn add_item(&mut self, symbol: &LitSym, item: Item) -> Result<(), Error> {
310        // if we are in the global scope, ensure we don't accidentally override builtin constants
311        // and functions. this is because there would be no way to access the builtin constants and
312        // functions after they are overridden
313
314        // it's ok to allow overriding in deeper scopes, as the user will still be able to access
315        // the builtin constants and functions afterward
316
317        // TODO: shadowing should probably be explicit (i.e. with `let` keyword)
318        if self.sym_table.is_global_scope() {
319            // existence of higher order functions means that a symbol could potentially override
320            // a builtin symbol or function, even if it is not explicitly declared as a function or
321            // symbol (see no_override_builtin test at bottom of file)
322            check_override_builtin(symbol)?;
323        }
324
325        // add to this final table
326        self.sym_table.insert(symbol.name.to_string(), item);
327        Ok(())
328    }
329
330    /// Creates a new scope in the symbol table. Within the provided function, all `compiler`
331    /// methods that add or mutate symbols will do so in the new scope.
332    ///
333    /// The scope is popped off the symbol table stack when the function returns. If no symbols
334    /// were added to the scope, it will not be added to the symbol table.
335    pub fn new_scope<F>(&mut self, f: F) -> Result<(), Error>
336        where F: FnOnce(&mut Compiler) -> Result<(), Error>
337    {
338        self.sym_table.enter_scope();
339        f(self)?;
340        self.sym_table.exit_scope();
341        Ok(())
342    }
343
344    /// Creates a new scope in the symbol table. Within the provided function, all `compiler`
345    /// methods that add or mutate symbols will do so in the new scope.
346    ///
347    /// The scope is popped when the function returns, and a reference to the scope is returned. It
348    /// will always be added to the symbol table, even if no symbols were added to it.
349    pub(crate) fn new_scope_get<F>(&mut self, f: F) -> Result<&Scope, Error>
350        where F: FnOnce(&mut Compiler) -> Result<(), Error>
351    {
352        self.sym_table.enter_scope();
353        f(self)?;
354        Ok(self.sym_table.exit_scope_get())
355    }
356
357    /// Creates a new chunk and a scope for compilation. Within the provided function, all
358    /// `compiler` methods that add or edit instructions will do so to the new chunk.
359    ///
360    /// Returns the unique identifier for the function and the index of the new chunk, which will
361    /// be used to add corresponding [`InstructionKind::LoadConst`] and [`InstructionKind::StoreVar`]
362    /// instructions to the parent chunk.
363    pub fn new_chunk<F>(&mut self, header: &FuncHeader, f: F) -> Result<NewChunk, Error>
364        where F: FnOnce(&mut Compiler) -> Result<(), Error>
365    {
366        let old_chunk_idx = self.chunk;
367        self.chunks.push(Chunk::new(header.params.len()));
368        let new_chunk_idx = self.chunks.len() - 1;
369
370        let id = self.next_item_id;
371        self.add_item(
372            &header.name,
373            Item::Func(FuncDecl::new(
374                id,
375                self.sym_table.next_id(),
376                new_chunk_idx,
377                header.params.clone(),
378            )),
379        )?;
380        self.next_item_id += 1;
381
382        self.chunk = new_chunk_idx;
383        let scope = self.new_scope_get(f)?;
384        let captures = scope.captures()
385            .iter()
386            .map(|symbol| match symbol {
387                Symbol::User(id) => *id,
388                _ => unreachable!(),
389            })
390            .collect();
391        self.chunk = old_chunk_idx;
392
393        Ok(NewChunk {
394            id,
395            chunk: new_chunk_idx,
396            captures,
397        })
398    }
399
400    /// Creates a new temporary chunk for compilation. Within the provided function, all `compiler`
401    /// methods that add or edit instructions will do so to the new chunk.
402    ///
403    /// The chunk is returned at the end of this function, without being added to the list of
404    /// chunks.
405    pub(crate) fn new_chunk_get<F>(&mut self, f: F) -> Result<Chunk, Error>
406        where F: FnOnce(&mut Compiler) -> Result<(), Error>
407    {
408        let old_chunk_idx = self.chunk;
409        self.chunks.push(Chunk::default());
410        let new_chunk_idx = self.chunks.len() - 1;
411
412        self.chunk = new_chunk_idx;
413        f(self)?;
414        let chunk = self.chunks.pop().unwrap();
415        self.chunk = old_chunk_idx;
416
417        Ok(chunk)
418    }
419
420    /// Adds a symbol to the symbol table at the current scope.
421    ///
422    /// This is a shortcut for [`Compiler::add_item`] that creates a new [`Item::Symbol`] from the
423    /// given symbol and returns the unique identifier for the symbol.
424    ///
425    /// # Manual compilation
426    ///
427    /// [`Compiler::add_symbol`] can be used to declare the existence of uninitialized variables.
428    /// This is useful for creating a symbol and acquiring its unique identifier in order to
429    /// manipulate it in a virtual machine.
430    ///
431    /// If you do this, you must ensure that the symbol is initialized before it is used. This can
432    /// be done in the virtual machine. See the [`cas-vm`] crate for an example.
433    ///
434    /// [`cas-vm`]: https://docs.rs/cas-vm/latest/cas_vm/
435    pub fn add_symbol(&mut self, symbol: &LitSym) -> Result<usize, Error> {
436        let id = self.next_item_id;
437        self.add_item(symbol, Item::Symbol(SymbolDecl { id }))?;
438        self.next_item_id += 1;
439        Ok(id)
440    }
441
442    /// Resolves a path to a user-created symbol, inserting it into the symbol table if it doesn't
443    /// exist.
444    ///
445    /// If the symbol name matches that of a builtin constant, one of the following will occur:
446    ///
447    /// - If this function is called from the global scope, an [`OverrideBuiltinConstant`]
448    ///   error is returned.
449    /// - If this function is called anywhere else, the symbol table will successfully be updated
450    ///   with the new symbol. This symbol shadows the existing builtin constant, meaning the
451    ///   builtin will not be accessible until the scope in which this symbol was declared, ends.
452    ///
453    /// Returns the unique identifier for the symbol, which can be used to reference the symbol in
454    /// the bytecode.
455    pub fn resolve_user_symbol_or_insert(&mut self, symbol: &LitSym) -> Result<usize, Error> {
456        if let Some(item) = self.sym_table.resolve_item(&symbol.name) {
457            // symbol was found in the current or a parent scope
458            Ok(item.id())
459        } else {
460            // if not, insert it
461            self.add_symbol(symbol)
462        }
463    }
464
465    /// Resolves a path to a symbol without inserting it into the symbol table. If the symbol is
466    /// determined to be captured from a parent scope, the enclosing function will be marked as
467    /// capturing the symbol.
468    ///
469    /// Returns the unique identifier for the symbol, or an error if the symbol is not found within
470    /// the current scope.
471    pub fn resolve_symbol(&mut self, symbol: &LitSym) -> Result<Symbol, Error> {
472        if let Some(symbol) = self.sym_table.resolve_item_mark_capture(&symbol.name) {
473            // symbol was found in the current or a parent scope
474            Ok(symbol)
475        } else {
476            // maybe it refers to a builtin constant or function
477            if let Some(symbol) = resolve_builtin(symbol) {
478                Ok(symbol)
479            } else {
480                // no matching symbol found
481                Err(Error::new(vec![symbol.span.clone()], UnknownVariable {
482                    name: symbol.name.clone(),
483                }))
484            }
485        }
486    }
487
488    /// Adds an instruction to the current chunk with no associated source code span.
489    pub fn add_instr(&mut self, instruction: impl Into<Instruction>) {
490        let chunk = self.chunk_mut();
491        chunk.instructions.push(instruction.into());
492    }
493
494    /// Adds an instruction to the current chunk with an associated source code span(s).
495    pub fn add_instr_with_spans(
496        &mut self,
497        instruction: impl Into<Instruction>,
498        spans: Vec<Range<usize>>,
499    ) {
500        let mut instruction = instruction.into();
501        instruction.spans = spans;
502        let chunk = self.chunk_mut();
503        chunk.instructions.push(instruction);
504    }
505
506    /// Adds a sequence of instructions from a chunk to the current chunk.
507    pub(crate) fn add_chunk_instrs(&mut self, new_chunk: Chunk) {
508        let chunk = self.chunk_mut();
509        chunk.instructions.extend(new_chunk.instructions);
510    }
511
512    /// Replaces an instruction at the given index in the current chunk with a new instruction.
513    pub fn replace_instr(&mut self, idx: usize, instruction: Instruction) {
514        let chunk = self.chunk_mut();
515        chunk.instructions[idx] = instruction;
516    }
517
518    /// Creates a unique label with no associated instruction. This label can be used to reference
519    /// a specific instruction in the bytecode.
520    pub fn new_unassociated_label(&mut self) -> Label {
521        let label = Label(self.labels.len());
522        self.labels.insert(label, None);
523        label
524    }
525
526    /// Creates a unique label pointing to the end of the currently generated bytecode in the
527    /// current chunk.
528    ///
529    /// When this method is called and [`Compile::compile`] is called immediately after, the label
530    /// will point to the first instruction generated by the compilation.
531    pub fn new_end_label(&mut self) -> Label {
532        let label = Label(self.labels.len());
533        let chunk_instrs = self.chunk().instructions.len();
534        self.labels.insert(label, Some((self.chunk, chunk_instrs)));
535        label
536    }
537
538    /// Associates the given label with the end of the currently generated bytecode.
539    ///
540    /// This is useful for creating labels that point to the end of a loop, for example.
541    pub fn set_end_label(&mut self, label: Label) {
542        let chunk_instrs = self.chunk().instructions.len();
543        self.labels.insert(label, Some((self.chunk, chunk_instrs)));
544    }
545}
546
547/// Trait for types that can be compiled into bytecode [`Instruction`]s.
548///
549/// The compiler is responsible for converting a CalcScript abstract syntax tree into a bytecode
550/// representation that can be executed by the
551/// [`Vm`](https://docs.rs/cas-vm/latest/cas_vm/vm/struct.Vm.html). The available instructions are
552/// defined in the [`InstructionKind`] `enum`.
553pub trait Compile {
554    /// Compiles the type into a sequence of [`Instruction`]s.
555    fn compile(&self, compiler: &mut Compiler) -> Result<(), Error>;
556}
557
558impl<T: Compile> Compile for &T {
559    fn compile(&self, compiler: &mut Compiler) -> Result<(), Error> {
560        (*self).compile(compiler)
561    }
562}
563
564#[cfg(test)]
565mod tests {
566    use super::*;
567    use cas_parser::parser::{ast::stmt::Stmt, Parser};
568
569    /// Compile the given source code.
570    fn compile(source: &str) -> Result<Compiler, Error> {
571        let mut parser = Parser::new(source);
572        let stmts = parser.try_parse_full_many::<Stmt>().unwrap();
573
574        Compiler::compile_program(stmts)
575    }
576
577    #[test]
578    fn function_declaration() {
579        compile("f(x) = {
580    g(x) = {
581        h(x) = x
582        h(x) % 2 == 0
583    }
584    x % 3 == 0 && g(x)
585}
586
587f(18)").unwrap();
588    }
589
590    #[test]
591    fn scoping() {
592        let err = compile("f() = j + 6
593g() = {
594    j = 10
595    f()
596}
597g()").unwrap_err();
598
599        // error is in the definition of `f`
600        // variable `j` is defined in `g`, so `f` can only access it if `x` is passed as an
601        // argument, or `j` is in a higher scope
602        assert_eq!(err.spans[0], 6..7);
603    }
604
605    #[test]
606    fn advanced_scoping() {
607        // `{}` curly braces, `f(x) = ...` function declarations, `loop`, `while`, `for`, `sum`,
608        // and `product` introduce new scopes
609        compile("{ x = 25 }; x").unwrap_err();
610
611        // but if a variable is already defined in a parent scope, it can be accessed in a child
612        // scope
613        compile("x = 25; { x *= 2 }; x").unwrap();
614
615        // this wouldn't make sense if a scope was not created
616        //
617        // functions aren't called at declaration, so `y` wouldn't be initialized when `y` is
618        // accessed
619        compile("f(x) = y = 25; y").unwrap_err();
620
621        compile("f(x) = { y = 25 }; y").unwrap_err();
622        compile("loop { t = rand(); if t < 0.2 break t }; t").unwrap_err();
623        compile("a = for i in 1..5 { t = i }; t").unwrap_err();
624
625        // `loop` and `while` _do_ introduce new scopes, specifically to avoid the first test
626        // below: if scopes were not introduced, `t` would be uninitialized after the loop
627        // immediately breaks, causing an error when `t` is accessed outside the loop
628        //
629        // despite that real issue, in general, scopes aren't all that useful in loops, since the
630        // loop body will usually need to have multiple statements. the user would have to use
631        // curly braces `{}` to write multiple statements, and thus, introduce a new scope
632        //
633        // otherwise, the loop body would just be a single statement, and the scope would be
634        compile("loop t = break 2; t").unwrap_err();
635        compile("while true break t = 2; t").unwrap_err();
636
637        compile("(sum n in 1..5 of n) > n").unwrap_err();
638    }
639
640    #[test]
641    fn scoping_with_compiler_declared_variables() {
642        compile("for n in 1..n then print(n)").unwrap_err();
643        compile("n = 50; for n in 0..n then print(n)").unwrap();
644        compile("for n in n..50 then print(n)").unwrap_err();
645    }
646
647    #[test]
648    fn shadowing() {
649        compile("pi = 5").unwrap_err();
650        compile("f() = pi = 5").unwrap(); // implicit shadowing occurs in non-global scopes
651    }
652
653    #[test]
654    fn no_override_builtin() {
655        compile("i = 5").unwrap_err(); // i is a builtin constant
656        compile("pi(x) = x").unwrap_err(); // pi is a builtin constant
657        compile("sqrt = 3").unwrap_err(); // sqrt is a builtin function
658        compile("ncr(a) = a").unwrap_err(); // ncr is a builtin function
659    }
660
661    #[test]
662    fn define_and_call() {
663        compile("f(x) = return x + 1/sqrt(x)
664g(x, y) = f(x) + f(y)
665g(2, 3)").unwrap();
666    }
667
668    #[test]
669    fn refer_to_parent() {
670        compile("f(x) = g(x) = h(x) = f(x)").unwrap();
671        compile("f(x) = g(x) = h(x) = g(x)").unwrap();
672        compile("f(x) = g(x) = h(x) = h(x)").unwrap();
673    }
674
675    #[test]
676    fn derivative() {
677        compile("f(x) = x^2; f'(2)").unwrap();
678        // TODO: it is not possible to derivate `ncr` (due to 2 parameters), so this should be an
679        // error
680        // however, ever since higher order functions were supported, functions couldn't be checked
681        // if they were valid for derivation until runtime
682        // however again, it is possible to determine if a function reference is referring to a
683        // builtin function, so technically we have enough information to determine if this is
684        // derivable at compile time. maybe we should do that
685        compile("ncr''(5, 3)").unwrap();
686    }
687
688    #[test]
689    fn list_index() {
690        compile("arr = [1, 2, 3]
691arr[0] = 5
692arr[0] + arr[1] + arr[2] == 10").unwrap();
693    }
694}