diff --git a/NEWS.md b/NEWS.md index 304313a2..da9abedb 100644 --- a/NEWS.md +++ b/NEWS.md @@ -57,6 +57,11 @@ STILL UNDER DEVELOPMENT; NOT RELEASED YET. * Fixed crashes when using the SDL 3 compatibility bindings for SDL 2. +* Removed the ability to call functions that may not take arguments with an + empty parameter list of the form `()`, to mimic the behavior of the vast + majority of BASIC dialects. This means that `RND()` is now illegal and + that users must call `RND` or `RND(1)`. + ## Changes in version 0.11.1 **Released on 2024-09-14.** diff --git a/cli/tests/lang/types.out b/cli/tests/lang/types.out index 87be56d5..ced11e11 100644 --- a/cli/tests/lang/types.out +++ b/cli/tests/lang/types.out @@ -24,7 +24,7 @@ ERROR: 1:30: Incompatible type annotation in a2# reference ERROR: 1:7: Incompatible type annotation in LEN$ reference >>> Invalid type access ERROR: 1:14: I is not an array nor a function -ERROR: 1:7: LEN expected expr$ +ERROR: 1:7: LEN expected (expr$) ERROR: 1:7: PRINT is not an array nor a function >>> Argless function calls 3.141592653589793 diff --git a/cli/tests/repl/help.out b/cli/tests/repl/help.out index 9a7248ad..af013195 100644 --- a/cli/tests/repl/help.out +++ b/cli/tests/repl/help.out @@ -663,7 +663,7 @@ Output from HELP "CLS": Output from HELP "COLOR": - COLOR <> | | <[fg%], [bg%]> + COLOR | fg% | [fg%], [bg%]  Sets the foreground and background colors. @@ -696,7 +696,7 @@ Output from HELP "DEG": Output from HELP "DIR": - DIR <> | + DIR | path$  Displays the list of files on the current or given path. @@ -771,7 +771,7 @@ Output from HELP "GFX_RECTF": Output from HELP "GFX_SYNC": - GFX_SYNC <> | + GFX_SYNC | enabled?  Controls the video syncing flag and/or forces a sync. @@ -799,7 +799,7 @@ Output from HELP "GFX_SYNC": Output from HELP "GPIO_CLEAR": - GPIO_CLEAR <> | + GPIO_CLEAR | pin%  Resets the GPIO chip or a specific pin. @@ -838,7 +838,7 @@ Output from HELP "GPIO_WRITE": Output from HELP "HELP": - HELP <> | + HELP | topic$  Prints interactive help. @@ -855,7 +855,7 @@ Output from HELP "HELP": Output from HELP "INPUT": - INPUT | <[prompt$] <,|;> vref> + INPUT vref | [prompt$] <,|;> vref  Obtains user input from the console. @@ -905,7 +905,7 @@ Output from HELP "LOCATE": Output from HELP "LOGIN": - LOGIN | + LOGIN username$ | username$, password$  Logs into the user's account. @@ -928,7 +928,7 @@ Output from HELP "LOGOUT": Output from HELP "MOUNT": - MOUNT <> | + MOUNT | target$ AS drive_name$  Lists the mounted drives or mounts a new drive. @@ -998,7 +998,7 @@ Output from HELP "RAD": Output from HELP "RANDOMIZE": - RANDOMIZE <> | + RANDOMIZE | seed%  Reinitializes the pseudo-random number generator. @@ -1052,7 +1052,7 @@ Output from HELP "RUN": Output from HELP "SAVE": - SAVE <> | + SAVE | filename$  Saves the current program in memory to the given filename. @@ -1116,7 +1116,7 @@ Output from HELP "UNMOUNT": Output from HELP "ASC": - ASC%(char$) + ASC% (char$)  Returns the UTF character code of the input character. @@ -1129,7 +1129,7 @@ Output from HELP "ASC": Output from HELP "ATN": - ATN#(n#) + ATN# (n#)  Computes the arc-tangent of a number. @@ -1138,7 +1138,7 @@ Output from HELP "ATN": Output from HELP "CHR": - CHR$(code%) + CHR$ (code%)  Returns the UTF character that corresponds to the given code. @@ -1146,7 +1146,7 @@ Output from HELP "CHR": Output from HELP "CINT": - CINT%(expr#) + CINT% (expr#)  Casts the given numeric expression to an integer (with rounding). @@ -1156,7 +1156,7 @@ Output from HELP "CINT": Output from HELP "COS": - COS#(angle#) + COS# (angle#)  Computes the cosine of an angle. @@ -1192,7 +1192,7 @@ Output from HELP "GFX_WIDTH": Output from HELP "GPIO_READ": - GPIO_READ?(pin%) + GPIO_READ? (pin%)  Reads the state of a GPIO pin. @@ -1226,7 +1226,7 @@ Output from HELP "INKEY": Output from HELP "INT%": - INT%(expr#) + INT% (expr#)  Casts the given numeric expression to an integer (with truncation). @@ -1238,7 +1238,7 @@ Output from HELP "INT%": Output from HELP "LBOUND": - LBOUND%( | ) + LBOUND% (array) | (array, dimension%)  Returns the lower bound for the given dimension of the array. @@ -1250,7 +1250,7 @@ Output from HELP "LBOUND": Output from HELP "LEFT": - LEFT$(expr$, n%) + LEFT$ (expr$, n%)  Returns a given number of characters from the left side of a string. @@ -1261,25 +1261,25 @@ Output from HELP "LEFT": Output from HELP "LEN": - LEN%(expr$) + LEN% (expr$)  Returns the length of the string in expr$. Output from HELP "LTRIM": - LTRIM$(expr$) + LTRIM$ (expr$)  Returns a copy of a string with leading whitespace removed. Output from HELP "MAX": - MAX#(expr1#[, .., exprN#]) + MAX# (expr1#[, .., exprN#])  Returns the maximum number out of a set of numbers. Output from HELP "MID": - MID$( | ) + MID$ (expr$, start%) | (expr$, start%, length%)  Returns a portion of a string. @@ -1293,7 +1293,7 @@ Output from HELP "MID": Output from HELP "MIN": - MIN#(expr1#[, .., exprN#]) + MIN# (expr1#[, .., exprN#])  Returns the minimum number out of a set of numbers. @@ -1305,7 +1305,7 @@ Output from HELP "PI": Output from HELP "RIGHT": - RIGHT$(expr$, n%) + RIGHT$ (expr$, n%)  Returns a given number of characters from the right side of a string. @@ -1316,7 +1316,7 @@ Output from HELP "RIGHT": Output from HELP "RND": - RND#(<> | ) + RND# | (n%)  Returns a random number in the [0..1] range. @@ -1331,7 +1331,7 @@ Output from HELP "RND": Output from HELP "RTRIM": - RTRIM$(expr$) + RTRIM$ (expr$)  Returns a copy of a string with trailing whitespace removed. @@ -1353,7 +1353,7 @@ Output from HELP "SCRROWS": Output from HELP "SIN": - SIN#(angle#) + SIN# (angle#)  Computes the sine of an angle. @@ -1362,13 +1362,13 @@ Output from HELP "SIN": Output from HELP "SQR": - SQR#(num#) + SQR# (num#)  Computes the square root of the given number. Output from HELP "STR$": - STR$(expr) + STR$ (expr)  Formats a scalar value as a string. @@ -1385,7 +1385,7 @@ Output from HELP "STR$": Output from HELP "TAN": - TAN#(angle#) + TAN# (angle#)  Computes the tangent of an angle. @@ -1394,7 +1394,7 @@ Output from HELP "TAN": Output from HELP "UBOUND": - UBOUND%( | ) + UBOUND% (array) | (array, dimension%)  Returns the upper bound for the given dimension of the array. diff --git a/core/src/bytecode.rs b/core/src/bytecode.rs index ac36291e..7512ea01 100644 --- a/core/src/bytecode.rs +++ b/core/src/bytecode.rs @@ -22,6 +22,23 @@ use crate::syms::SymbolKey; /// Convenience type to represent a program address. pub type Address = usize; +/// Components of an array indexing operation. +#[derive(Debug, PartialEq)] +#[cfg_attr(test, derive(Clone))] +pub struct ArrayIndexISpan { + /// Name of the array to index. + pub name: SymbolKey, + + /// Position of the name. + pub name_pos: LineCol, + + /// Index of `name` on the stack. + pub index: usize, + + /// Number of subscripts on the stack to index the array. + pub nargs: usize, +} + /// Components of a builtin command call. #[derive(Debug, PartialEq)] #[cfg_attr(test, derive(Clone))] @@ -49,6 +66,9 @@ pub struct DimISpan { /// Name of the variable to define. pub name: SymbolKey, + /// Index of `name` on the stack. + pub index: usize, + /// Whether the variable is global or not. pub shared: bool, @@ -66,6 +86,9 @@ pub struct DimArrayISpan { /// Position of the name. pub name_pos: LineCol, + /// Index of `name` on the stack. + pub index: usize, + /// Whether the array is global or not. pub shared: bool, @@ -79,6 +102,20 @@ pub struct DimArrayISpan { pub subtype_pos: LineCol, } +/// Components of a change to the error handler. +#[derive(Clone, Copy)] +#[cfg_attr(test, derive(Debug, Eq, PartialEq))] +pub enum ErrorHandlerISpan { + /// Jumps to the included address on error. + Jump(Address), + + /// Sets the error handler to the default. + None, + + /// Sets the error handler to resume execution at to the next instruction. + ResumeNext, +} + /// Components of a builtin function call. #[derive(Debug, PartialEq)] #[cfg_attr(test, derive(Clone))] @@ -112,22 +149,24 @@ pub struct JumpIfDefinedISpan { /// The variable to check for nonexistence. pub var: SymbolKey, + /// Index of `var` on the stack. + pub index: usize, + /// The address to jump to. pub addr: Address, } -/// Components of a change to the error handler. -#[derive(Clone, Copy)] +/// Components of a load operation. #[cfg_attr(test, derive(Debug, Eq, PartialEq))] -pub enum ErrorHandlerISpan { - /// Jumps to the included address on error. - Jump(Address), +pub struct LoadISpan { + /// Name of the variable to load. + pub name: SymbolKey, - /// Sets the error handler to the default. - None, + /// Position of where this instruction was requested. + pub pos: LineCol, - /// Sets the error handler to resume execution at to the next instruction. - ResumeNext, + /// Index of `name` on the stack. + pub index: usize, } /// Components of a request to unset a variable. @@ -138,6 +177,9 @@ pub struct UnsetISpan { /// Position of where this instruction was requested. pub pos: LineCol, + + /// Index of `name` on the stack. + pub index: usize, } /// Representation of all possible instructions in the bytecode. @@ -279,13 +321,13 @@ pub enum Instruction { ConcatStrings(LineCol), /// Represents an assignment to an element of an array with the given number of subscripts. - ArrayAssignment(SymbolKey, LineCol, usize), + ArrayAssignment(ArrayIndexISpan), /// Represents a load of an array's element into the stack. - ArrayLoad(SymbolKey, LineCol, usize), + ArrayLoad(ArrayIndexISpan), /// Represents an assignment of a value to a variable. - Assign(SymbolKey), + Assign(SymbolKey, usize), /// Represents a call to a builtin command such as `PRINT` with the given number of arguments. BuiltinCall(BuiltinCallISpan), @@ -331,19 +373,19 @@ pub enum Instruction { LeaveScope, /// Represents a load of a boolean variable's value from main memory into the stack. - LoadBoolean(SymbolKey, LineCol), + LoadBoolean(LoadISpan), /// Represents a load of a double variable's value from main memory into the stack. - LoadDouble(SymbolKey, LineCol), + LoadDouble(LoadISpan), /// Represents a load of an integer variable's value from main memory into the stack. - LoadInteger(SymbolKey, LineCol), + LoadInteger(LoadISpan), /// Represents a load of a string variable's value from main memory into the stack. - LoadString(SymbolKey, LineCol), + LoadString(LoadISpan), /// Represents a load of a variable's reference into the stack. - LoadRef(SymbolKey, ExprType, LineCol), + LoadRef(LoadISpan, ExprType), /// Represents an instruction that does nothing. Nop, @@ -429,15 +471,15 @@ impl Instruction { Instruction::ConcatStrings(_pos) => ("CONCAT$", None), - Instruction::ArrayAssignment(key, _pos, nargs) => { - ("SETA", Some(format!("{}, {}", key, nargs))) + Instruction::ArrayAssignment(span) => { + ("SETA", Some(format!("{}, {}", span.name, span.nargs))) } - Instruction::ArrayLoad(key, _pos, nargs) => { - ("LOADA", Some(format!("{}, {}", key, nargs))) + Instruction::ArrayLoad(span) => { + ("LOADA", Some(format!("{}, {}", span.name, span.nargs))) } - Instruction::Assign(key) => ("SETV", Some(key.to_string())), + Instruction::Assign(key, _index) => ("SETV", Some(key.to_string())), Instruction::BuiltinCall(span) => { ("CALLB", Some(format!("{} ({}), {}", span.upcall_index, span.name, span.nargs))) @@ -499,12 +541,12 @@ impl Instruction { Instruction::LeaveScope => ("LEAVE", None), - Instruction::LoadBoolean(key, _pos) => ("LOAD?", Some(key.to_string())), - Instruction::LoadDouble(key, _pos) => ("LOAD#", Some(key.to_string())), - Instruction::LoadInteger(key, _pos) => ("LOAD%", Some(key.to_string())), - Instruction::LoadString(key, _pos) => ("LOAD$", Some(key.to_string())), + Instruction::LoadBoolean(span) => ("LOAD?", Some(span.name.to_string())), + Instruction::LoadDouble(span) => ("LOAD#", Some(span.name.to_string())), + Instruction::LoadInteger(span) => ("LOAD%", Some(span.name.to_string())), + Instruction::LoadString(span) => ("LOAD$", Some(span.name.to_string())), - Instruction::LoadRef(key, _etype, _pos) => ("LOADR", Some(key.to_string())), + Instruction::LoadRef(span, _etype) => ("LOADR", Some(span.name.to_string())), Instruction::Nop => ("NOP", None), @@ -585,9 +627,9 @@ impl Instruction { Instruction::ConcatStrings(pos) => Some(*pos), - Instruction::ArrayAssignment(_, pos, _) => Some(*pos), - Instruction::ArrayLoad(_, pos, _) => Some(*pos), - Instruction::Assign(_) => None, + Instruction::ArrayAssignment(span) => Some(span.name_pos), + Instruction::ArrayLoad(span) => Some(span.name_pos), + Instruction::Assign(..) => None, Instruction::BuiltinCall(span) => Some(span.name_pos), Instruction::Call(_) => None, Instruction::FunctionCall(span) => Some(span.name_pos), @@ -602,11 +644,11 @@ impl Instruction { Instruction::JumpIfTrue(_) => None, Instruction::JumpIfNotTrue(_) => None, Instruction::LeaveScope => None, - Instruction::LoadBoolean(_, pos) => Some(*pos), - Instruction::LoadDouble(_, pos) => Some(*pos), - Instruction::LoadInteger(_, pos) => Some(*pos), - Instruction::LoadString(_, pos) => Some(*pos), - Instruction::LoadRef(_, _, pos) => Some(*pos), + Instruction::LoadBoolean(span) => Some(span.pos), + Instruction::LoadDouble(span) => Some(span.pos), + Instruction::LoadInteger(span) => Some(span.pos), + Instruction::LoadString(span) => Some(span.pos), + Instruction::LoadRef(span, _) => Some(span.pos), Instruction::Nop => None, Instruction::PushBoolean(_, pos) => Some(*pos), Instruction::PushDouble(_, pos) => Some(*pos), @@ -633,7 +675,7 @@ impl Instruction { | Instruction::BitwiseOr(_) | Instruction::BitwiseXor(_) | Instruction::BitwiseNot(_) - | Instruction::ArrayLoad(_, _, _) + | Instruction::ArrayLoad(..) | Instruction::ShiftLeft(_) | Instruction::ShiftRight(_) | Instruction::EqualBooleans(_) @@ -674,11 +716,11 @@ impl Instruction { | Instruction::FunctionCall(_) | Instruction::DoubleToInteger | Instruction::IntegerToDouble - | Instruction::LoadBoolean(_, _) - | Instruction::LoadDouble(_, _) - | Instruction::LoadInteger(_, _) - | Instruction::LoadString(_, _) - | Instruction::LoadRef(_, _, _) + | Instruction::LoadBoolean(..) + | Instruction::LoadDouble(..) + | Instruction::LoadInteger(..) + | Instruction::LoadString(..) + | Instruction::LoadRef(..) | Instruction::PushBoolean(_, _) | Instruction::PushDouble(_, _) | Instruction::PushInteger(_, _) @@ -686,8 +728,8 @@ impl Instruction { | Instruction::EnterScope | Instruction::LeaveScope => false, - Instruction::ArrayAssignment(_, _, _) - | Instruction::Assign(_) + Instruction::ArrayAssignment(..) + | Instruction::Assign(..) | Instruction::BuiltinCall(_) | Instruction::Call(_) | Instruction::Dim(_) diff --git a/core/src/compiler/args.rs b/core/src/compiler/args.rs index 9c74204c..b98780c3 100644 --- a/core/src/compiler/args.rs +++ b/core/src/compiler/args.rs @@ -328,11 +328,13 @@ impl CallableSyntax { /// If the reference does not exist and the syntax allowed undefined symbols, returns the details /// for the symbol to insert into the symbols table, which the caller must handle because we do /// not have mutable access to the `symtable` here. +#[allow(clippy::too_many_arguments)] fn compile_required_ref( instrs: &mut Vec, md: &CallableMetadata, pos: LineCol, symtable: &SymbolsTable, + next_index_offset: usize, require_array: bool, define_undefined: bool, expr: Option, @@ -340,7 +342,7 @@ fn compile_required_ref( match expr { Some(Expr::Symbol(span)) => { let key = SymbolKey::from(span.vref.name()); - match symtable.get(&key) { + match symtable.get_with_index(&key) { None => { if !define_undefined { return Err(Error::UndefinedSymbol(span.pos, key)); @@ -355,11 +357,16 @@ fn compile_required_ref( )); } - instrs.push(Instruction::LoadRef(key.clone(), vtype, span.pos)); - Ok(Some((key, SymbolPrototype::Variable(vtype)))) + let proto = SymbolPrototype::Variable(vtype); + let next_index = symtable.last_index_of(&proto) + next_index_offset; + instrs.push(Instruction::LoadRef( + LoadISpan { name: key.clone(), pos: span.pos, index: next_index }, + vtype, + )); + Ok(Some((key, proto))) } - Some(SymbolPrototype::Array(vtype, _)) => { + Some((SymbolPrototype::Array(vtype, _), index)) => { let vtype = *vtype; if !span.vref.accepts(vtype) { @@ -372,11 +379,14 @@ fn compile_required_ref( return Err(Error::NotAReference(span.pos)); } - instrs.push(Instruction::LoadRef(key, vtype, span.pos)); + instrs.push(Instruction::LoadRef( + LoadISpan { name: key.clone(), pos: span.pos, index }, + vtype, + )); Ok(None) } - Some(SymbolPrototype::Variable(vtype)) => { + Some((SymbolPrototype::Variable(vtype), index)) => { let vtype = *vtype; if !span.vref.accepts(vtype) { @@ -389,12 +399,15 @@ fn compile_required_ref( return Err(Error::NotAReference(span.pos)); } - instrs.push(Instruction::LoadRef(key, vtype, span.pos)); + instrs.push(Instruction::LoadRef( + LoadISpan { name: key.clone(), pos: span.pos, index }, + vtype, + )); Ok(None) } - Some(SymbolPrototype::BuiltinCallable(md, _)) - | Some(SymbolPrototype::Callable(md)) => { + Some((SymbolPrototype::BuiltinCallable(md), _index)) + | Some((SymbolPrototype::Callable(md), _index)) => { if !span.vref.accepts_callable(md.return_type()) { return Err(Error::IncompatibleTypeAnnotationInReference( span.pos, span.vref, @@ -540,6 +553,7 @@ fn compile_args( md, pos, symtable, + to_insert.len(), false, true, Some(expr), @@ -613,6 +627,7 @@ fn compile_args( md, pos, symtable, + 0, details.require_array, details.define_undefined, span.expr, @@ -1192,7 +1207,10 @@ mod compile_tests { sep: ArgSep::End, sep_pos: lc(1, 5), }]) - .exp_instr(Instruction::LoadRef(SymbolKey::from("foo"), ExprType::Text, lc(1, 2))) + .exp_instr(Instruction::LoadRef( + LoadISpan { name: SymbolKey::from("foo"), pos: lc(1, 2), index: 0 }, + ExprType::Text, + )) .exp_nargs(1) .check(); } @@ -1325,7 +1343,10 @@ mod compile_tests { sep: ArgSep::End, sep_pos: lc(1, 5), }]) - .exp_instr(Instruction::LoadRef(SymbolKey::from("foo"), ExprType::Integer, lc(1, 2))) + .exp_instr(Instruction::LoadRef( + LoadISpan { name: SymbolKey::from("foo"), pos: lc(1, 2), index: 0 }, + ExprType::Integer, + )) .exp_nargs(1) .exp_symbol("foo", ExprType::Integer) .check(); @@ -1353,7 +1374,10 @@ mod compile_tests { sep: ArgSep::End, sep_pos: lc(1, 6), }]) - .exp_instr(Instruction::LoadRef(SymbolKey::from("foo"), ExprType::Text, lc(1, 2))) + .exp_instr(Instruction::LoadRef( + LoadISpan { name: SymbolKey::from("foo"), pos: lc(1, 2), index: 0 }, + ExprType::Text, + )) .exp_nargs(1) .exp_symbol("foo", ExprType::Text) .check(); @@ -1401,8 +1425,14 @@ mod compile_tests { sep_pos: lc(1, 5), }, ]) - .exp_instr(Instruction::LoadRef(SymbolKey::from("foo"), ExprType::Integer, lc(1, 2))) - .exp_instr(Instruction::LoadRef(SymbolKey::from("foo"), ExprType::Integer, lc(1, 2))) + .exp_instr(Instruction::LoadRef( + LoadISpan { name: SymbolKey::from("foo"), pos: lc(1, 2), index: 0 }, + ExprType::Integer, + )) + .exp_instr(Instruction::LoadRef( + LoadISpan { name: SymbolKey::from("foo"), pos: lc(1, 2), index: 0 }, + ExprType::Integer, + )) .exp_nargs(2) .exp_symbol("foo", ExprType::Integer) .check(); @@ -1431,7 +1461,10 @@ mod compile_tests { sep: ArgSep::End, sep_pos: lc(1, 5), }]) - .exp_instr(Instruction::LoadRef(SymbolKey::from("foo"), ExprType::Text, lc(1, 2))) + .exp_instr(Instruction::LoadRef( + LoadISpan { name: SymbolKey::from("foo"), pos: lc(1, 2), index: 0 }, + ExprType::Text, + )) .exp_nargs(1) .check(); } @@ -1964,7 +1997,10 @@ mod compile_tests { sep: ArgSep::End, sep_pos: lc(1, 2), }]) - .exp_instr(Instruction::LoadRef(SymbolKey::from("foo"), ExprType::Text, lc(1, 2))) + .exp_instr(Instruction::LoadRef( + LoadISpan { name: SymbolKey::from("foo"), pos: lc(1, 2), index: 0 }, + ExprType::Text, + )) .exp_nargs(1) .check(); } diff --git a/core/src/compiler/exprs.rs b/core/src/compiler/exprs.rs index a6c5caea..6681b12c 100644 --- a/core/src/compiler/exprs.rs +++ b/core/src/compiler/exprs.rs @@ -353,20 +353,26 @@ fn compile_expr_symbol( allow_varrefs: bool, ) -> Result { let key = SymbolKey::from(span.vref.name()); - let (instr, vtype) = match symtable.get(&key) { + let (instr, vtype) = match symtable.get_with_index(&key) { None => return Err(Error::UndefinedSymbol(span.pos, key)), - Some(SymbolPrototype::Array(atype, _dims)) => { + Some((SymbolPrototype::Array(atype, _dims), index)) => { if allow_varrefs { - (Instruction::LoadRef(key, *atype, span.pos), *atype) + ( + Instruction::LoadRef(LoadISpan { name: key, pos: span.pos, index }, *atype), + *atype, + ) } else { return Err(Error::NotAVariable(span.pos, span.vref)); } } - Some(SymbolPrototype::Variable(vtype)) => { + Some((SymbolPrototype::Variable(vtype), index)) => { if allow_varrefs { - (Instruction::LoadRef(key, *vtype, span.pos), *vtype) + ( + Instruction::LoadRef(LoadISpan { name: key, pos: span.pos, index }, *vtype), + *vtype, + ) } else { let instr = match vtype { ExprType::Boolean => Instruction::LoadBoolean, @@ -374,11 +380,11 @@ fn compile_expr_symbol( ExprType::Integer => Instruction::LoadInteger, ExprType::Text => Instruction::LoadString, }; - (instr(key, span.pos), *vtype) + (instr(LoadISpan { name: key, pos: span.pos, index }), *vtype) } } - Some(SymbolPrototype::BuiltinCallable(md, upcall_index)) => { + Some((SymbolPrototype::BuiltinCallable(md), upcall_index)) => { let etype = match md.return_type() { Some(etype) => etype, None => { @@ -396,7 +402,7 @@ fn compile_expr_symbol( Instruction::FunctionCall(FunctionCallISpan { name: key, name_pos: span.pos, - upcall_index: *upcall_index, + upcall_index, return_type: etype, nargs: 0, }), @@ -404,7 +410,7 @@ fn compile_expr_symbol( ) } - Some(SymbolPrototype::Callable(md)) => { + Some((SymbolPrototype::Callable(md), _index)) => { let etype = match md.return_type() { Some(etype) => etype, None => { @@ -430,12 +436,14 @@ fn compile_expr_symbol( } /// Compiles an array access. +#[allow(clippy::too_many_arguments)] fn compile_array_ref( instrs: &mut Vec, fixups: &mut HashMap, symtable: &SymbolsTable, span: CallSpan, key: SymbolKey, + index: usize, vtype: ExprType, dimensions: usize, ) -> Result { @@ -446,7 +454,12 @@ fn compile_array_ref( if !span.vref.accepts(vtype) { return Err(Error::IncompatibleTypeAnnotationInReference(span.vref_pos, span.vref)); } - instrs.push(Instruction::ArrayLoad(key, span.vref_pos, nargs)); + instrs.push(Instruction::ArrayLoad(ArrayIndexISpan { + name: key, + name_pos: span.vref_pos, + index, + nargs, + })); Ok(vtype) } @@ -700,12 +713,12 @@ pub(super) fn compile_expr( Expr::Call(span) => { let key = SymbolKey::from(span.vref.name()); - match symtable.get(&key) { - Some(SymbolPrototype::Array(vtype, dims)) => { - compile_array_ref(instrs, fixups, symtable, span, key, *vtype, *dims) + match symtable.get_with_index(&key) { + Some((SymbolPrototype::Array(vtype, dims), index)) => { + compile_array_ref(instrs, fixups, symtable, span, key, index, *vtype, *dims) } - Some(SymbolPrototype::BuiltinCallable(md, upcall_index)) => { + Some((SymbolPrototype::BuiltinCallable(md), upcall_index)) => { if !span.vref.accepts_callable(md.return_type()) { return Err(Error::IncompatibleTypeAnnotationInReference( span.vref_pos, @@ -720,24 +733,23 @@ pub(super) fn compile_expr( } }; - if md.is_argless() { - return Err(Error::CallableSyntaxError(span.vref_pos, md.clone())); - } - let span_pos = span.vref_pos; let nargs = compile_function_args(md, instrs, fixups, symtable, span_pos, span.args)?; + if md.is_argless() && nargs == 0 { + return Err(Error::CallableSyntaxError(span.vref_pos, md.clone())); + } instrs.push(Instruction::FunctionCall(FunctionCallISpan { name: key, name_pos: span_pos, - upcall_index: *upcall_index, + upcall_index, return_type: vtype, nargs, })); Ok(vtype) } - Some(SymbolPrototype::Callable(md)) => { + Some((SymbolPrototype::Callable(md), _index)) => { if !span.vref.accepts_callable(md.return_type()) { return Err(Error::IncompatibleTypeAnnotationInReference( span.vref_pos, @@ -752,18 +764,18 @@ pub(super) fn compile_expr( } }; - if md.is_argless() { + let span_pos = span.vref_pos; + let nargs = + compile_function_args(md, instrs, fixups, symtable, span_pos, span.args)?; + if md.is_argless() && nargs == 0 { return Err(Error::CallableSyntaxError(span.vref_pos, md.clone())); } - - let span_pos = span.vref_pos; - compile_function_args(md, instrs, fixups, symtable, span_pos, span.args)?; instrs.push(Instruction::Nop); fixups.insert(instrs.len() - 1, Fixup::Call(key, span_pos)); Ok(vtype) } - Some(SymbolPrototype::Variable(_)) => { + Some((SymbolPrototype::Variable(_), _index)) => { Err(Error::NotArrayOrFunction(span.vref_pos, key)) } @@ -822,13 +834,13 @@ mod tests { .parse("b = TRUE\nd = 2.3\ni = 5\nt = \"foo\"") .compile() .expect_instr(0, Instruction::PushBoolean(true, lc(1, 5))) - .expect_instr(1, Instruction::Assign(SymbolKey::from("b"))) + .expect_instr(1, Instruction::Assign(SymbolKey::from("b"), 0)) .expect_instr(2, Instruction::PushDouble(2.3, lc(2, 5))) - .expect_instr(3, Instruction::Assign(SymbolKey::from("d"))) + .expect_instr(3, Instruction::Assign(SymbolKey::from("d"), 1)) .expect_instr(4, Instruction::PushInteger(5, lc(3, 5))) - .expect_instr(5, Instruction::Assign(SymbolKey::from("i"))) + .expect_instr(5, Instruction::Assign(SymbolKey::from("i"), 2)) .expect_instr(6, Instruction::PushString("foo".to_owned(), lc(4, 5))) - .expect_instr(7, Instruction::Assign(SymbolKey::from("t"))) + .expect_instr(7, Instruction::Assign(SymbolKey::from("t"), 3)) .check(); } @@ -838,8 +850,15 @@ mod tests { .define("j", SymbolPrototype::Variable(ExprType::Integer)) .parse("i = j") .compile() - .expect_instr(0, Instruction::LoadInteger(SymbolKey::from("j"), lc(1, 5))) - .expect_instr(1, Instruction::Assign(SymbolKey::from("i"))) + .expect_instr( + 0, + Instruction::LoadInteger(LoadISpan { + name: SymbolKey::from("j"), + pos: lc(1, 5), + index: 0, + }), + ) + .expect_instr(1, Instruction::Assign(SymbolKey::from("i"), 1)) .check(); } @@ -859,7 +878,56 @@ mod tests { nargs: 0, }), ) - .expect_instr(1, Instruction::Assign(SymbolKey::from("i"))) + .expect_instr(1, Instruction::Assign(SymbolKey::from("i"), 0)) + .check(); + } + + #[test] + fn test_compile_expr_argless_and_not_argless_ok() { + Tester::default() + .define_callable( + CallableMetadataBuilder::new("F").with_return_type(ExprType::Integer).with_syntax( + &[ + (&[], None), + ( + &[SingularArgSyntax::RequiredValue( + RequiredValueSyntax { + name: Cow::Borrowed("i"), + vtype: ExprType::Integer, + }, + ArgSepSyntax::End, + )], + None, + ), + ], + ), + ) + .parse("i = f") + .parse("j = f(3)") + .compile() + .expect_instr( + 0, + Instruction::FunctionCall(FunctionCallISpan { + name: SymbolKey::from("f"), + name_pos: lc(1, 5), + upcall_index: 0, + return_type: ExprType::Integer, + nargs: 0, + }), + ) + .expect_instr(1, Instruction::Assign(SymbolKey::from("i"), 0)) + .expect_instr(2, Instruction::PushInteger(3, lc(2, 7))) + .expect_instr( + 3, + Instruction::FunctionCall(FunctionCallISpan { + name: SymbolKey::from("f"), + name_pos: lc(2, 5), + upcall_index: 0, + return_type: ExprType::Integer, + nargs: 1, + }), + ) + .expect_instr(4, Instruction::Assign(SymbolKey::from("j"), 1)) .check(); } @@ -882,7 +950,7 @@ mod tests { ) .parse("i = f") .compile() - .expect_err("1:5: F expected i%") + .expect_err("1:5: F expected (i%)") .check(); } @@ -996,7 +1064,10 @@ mod tests { .compile() .expect_instr( 0, - Instruction::LoadRef(SymbolKey::from("a"), ExprType::Integer, lc(1, 3)), + Instruction::LoadRef( + LoadISpan { name: SymbolKey::from("a"), pos: lc(1, 3), index: 0 }, + ExprType::Integer, + ), ) .expect_instr( 1, @@ -1049,14 +1120,35 @@ mod tests { .parse("b = true OR x AND y XOR NOT z") .compile() .expect_instr(0, Instruction::PushBoolean(true, lc(1, 5))) - .expect_instr(1, Instruction::LoadBoolean(SymbolKey::from("x"), lc(1, 13))) + .expect_instr( + 1, + Instruction::LoadBoolean(LoadISpan { + name: SymbolKey::from("x"), + pos: lc(1, 13), + index: 0, + }), + ) .expect_instr(2, Instruction::LogicalOr(lc(1, 10))) - .expect_instr(3, Instruction::LoadBoolean(SymbolKey::from("y"), lc(1, 19))) + .expect_instr( + 3, + Instruction::LoadBoolean(LoadISpan { + name: SymbolKey::from("y"), + pos: lc(1, 19), + index: 1, + }), + ) .expect_instr(4, Instruction::LogicalAnd(lc(1, 15))) - .expect_instr(5, Instruction::LoadBoolean(SymbolKey::from("z"), lc(1, 29))) + .expect_instr( + 5, + Instruction::LoadBoolean(LoadISpan { + name: SymbolKey::from("z"), + pos: lc(1, 29), + index: 2, + }), + ) .expect_instr(6, Instruction::LogicalNot(lc(1, 25))) .expect_instr(7, Instruction::LogicalXor(lc(1, 21))) - .expect_instr(8, Instruction::Assign(SymbolKey::from("b"))) + .expect_instr(8, Instruction::Assign(SymbolKey::from("b"), 3)) .check(); } @@ -1067,12 +1159,26 @@ mod tests { .define("b", SymbolPrototype::Variable(ExprType::Integer)) .parse("i = a >> 5 << b") .compile() - .expect_instr(0, Instruction::LoadInteger(SymbolKey::from("a"), lc(1, 5))) + .expect_instr( + 0, + Instruction::LoadInteger(LoadISpan { + name: SymbolKey::from("a"), + pos: lc(1, 5), + index: 0, + }), + ) .expect_instr(1, Instruction::PushInteger(5, lc(1, 10))) .expect_instr(2, Instruction::ShiftRight(lc(1, 7))) - .expect_instr(3, Instruction::LoadInteger(SymbolKey::from("b"), lc(1, 15))) + .expect_instr( + 3, + Instruction::LoadInteger(LoadISpan { + name: SymbolKey::from("b"), + pos: lc(1, 15), + index: 1, + }), + ) .expect_instr(4, Instruction::ShiftLeft(lc(1, 12))) - .expect_instr(5, Instruction::Assign(SymbolKey::from("i"))) + .expect_instr(5, Instruction::Assign(SymbolKey::from("i"), 2)) .check(); } @@ -1088,7 +1194,7 @@ mod tests { /// is the same value as represented in EndBASIC code, and `test_value_type` is the /// corresponding type definition. fn do_op_test< - L: Fn(SymbolKey, LineCol) -> Instruction, + L: Fn(LoadISpan) -> Instruction, M: Fn(LineCol) -> Instruction, P: Fn(V, LineCol) -> Instruction, V, @@ -1105,10 +1211,13 @@ mod tests { .define("a", SymbolPrototype::Variable(test_value_type)) .parse(&format!("b = a {} {}", op_name, test_value_str)) .compile() - .expect_instr(0, load_inst(SymbolKey::from("a"), lc(1, 5))) + .expect_instr( + 0, + load_inst(LoadISpan { name: SymbolKey::from("a"), pos: lc(1, 5), index: 0 }), + ) .expect_instr(1, push_inst(test_value, lc(1, 8 + op_name.len()))) .expect_instr(2, op_inst(lc(1, 7))) - .expect_instr(3, Instruction::Assign(SymbolKey::from("b"))) + .expect_instr(3, Instruction::Assign(SymbolKey::from("b"), 1)) .check(); } @@ -1192,7 +1301,7 @@ mod tests { .compile() .expect_instr(0, PushDouble(60.2, lc(1, 6))) .expect_instr(1, NegateDouble(lc(1, 5))) - .expect_instr(2, Assign(SymbolKey::from("i"))) + .expect_instr(2, Assign(SymbolKey::from("i"), 1)) .check(); do_op_test(LoadInteger, AddIntegers, "+", PushInteger, 10, "10", Integer); @@ -1207,7 +1316,7 @@ mod tests { .compile() .expect_instr(0, PushInteger(60, lc(1, 6))) .expect_instr(1, NegateInteger(lc(1, 5))) - .expect_instr(2, Assign(SymbolKey::from("i"))) + .expect_instr(2, Assign(SymbolKey::from("i"), 1)) .check(); do_op_test(LoadString, ConcatStrings, "+", PushString, "foo".to_owned(), "\"foo\"", Text); @@ -1221,13 +1330,35 @@ mod tests { .define("k", SymbolPrototype::Variable(ExprType::Integer)) .parse("i = FOO(3, j, k + 1)") .compile() - .expect_instr(0, Instruction::LoadInteger(SymbolKey::from("k"), lc(1, 15))) + .expect_instr( + 0, + Instruction::LoadInteger(LoadISpan { + name: SymbolKey::from("k"), + pos: lc(1, 15), + index: 2, + }), + ) .expect_instr(1, Instruction::PushInteger(1, lc(1, 19))) .expect_instr(2, Instruction::AddIntegers(lc(1, 17))) - .expect_instr(3, Instruction::LoadInteger(SymbolKey::from("j"), lc(1, 12))) + .expect_instr( + 3, + Instruction::LoadInteger(LoadISpan { + name: SymbolKey::from("j"), + pos: lc(1, 12), + index: 1, + }), + ) .expect_instr(4, Instruction::PushInteger(3, lc(1, 9))) - .expect_instr(5, Instruction::ArrayLoad(SymbolKey::from("foo"), lc(1, 5), 3)) - .expect_instr(6, Instruction::Assign(SymbolKey::from("i"))) + .expect_instr( + 5, + Instruction::ArrayLoad(ArrayIndexISpan { + name: SymbolKey::from("foo"), + name_pos: lc(1, 5), + index: 0, + nargs: 3, + }), + ) + .expect_instr(6, Instruction::Assign(SymbolKey::from("i"), 3)) .check(); } @@ -1238,8 +1369,16 @@ mod tests { .parse("i = FOO%(3)") .compile() .expect_instr(0, Instruction::PushInteger(3, lc(1, 10))) - .expect_instr(1, Instruction::ArrayLoad(SymbolKey::from("foo"), lc(1, 5), 1)) - .expect_instr(2, Instruction::Assign(SymbolKey::from("i"))) + .expect_instr( + 1, + Instruction::ArrayLoad(ArrayIndexISpan { + name: SymbolKey::from("foo"), + name_pos: lc(1, 5), + index: 0, + nargs: 1, + }), + ) + .expect_instr(2, Instruction::Assign(SymbolKey::from("i"), 1)) .check(); } @@ -1261,8 +1400,16 @@ mod tests { .compile() .expect_instr(0, Instruction::PushDouble(3.8, lc(1, 9))) .expect_instr(1, Instruction::DoubleToInteger) - .expect_instr(2, Instruction::ArrayLoad(SymbolKey::from("foo"), lc(1, 5), 1)) - .expect_instr(3, Instruction::Assign(SymbolKey::from("i"))) + .expect_instr( + 2, + Instruction::ArrayLoad(ArrayIndexISpan { + name: SymbolKey::from("foo"), + name_pos: lc(1, 5), + index: 0, + nargs: 1, + }), + ) + .expect_instr(3, Instruction::Assign(SymbolKey::from("i"), 1)) .check(); } @@ -1322,12 +1469,26 @@ mod tests { .define("k", SymbolPrototype::Variable(ExprType::Double)) .parse("i = FOO(3, j, k + 1)") .compile() - .expect_instr(0, Instruction::LoadDouble(SymbolKey::from("k"), lc(1, 15))) + .expect_instr( + 0, + Instruction::LoadDouble(LoadISpan { + name: SymbolKey::from("k"), + pos: lc(1, 15), + index: 1, + }), + ) .expect_instr(1, Instruction::PushInteger(1, lc(1, 19))) .expect_instr(2, Instruction::IntegerToDouble) .expect_instr(3, Instruction::AddDoubles(lc(1, 17))) .expect_instr(4, Instruction::DoubleToInteger) - .expect_instr(5, Instruction::LoadInteger(SymbolKey::from("j"), lc(1, 12))) + .expect_instr( + 5, + Instruction::LoadInteger(LoadISpan { + name: SymbolKey::from("j"), + pos: lc(1, 12), + index: 0, + }), + ) .expect_instr(6, Instruction::PushInteger(3, lc(1, 9))) .expect_instr( 7, @@ -1339,7 +1500,7 @@ mod tests { nargs: 3, }), ) - .expect_instr(8, Instruction::Assign(SymbolKey::from("i"))) + .expect_instr(8, Instruction::Assign(SymbolKey::from("i"), 2)) .check(); } diff --git a/core/src/compiler/mod.rs b/core/src/compiler/mod.rs index 7dbd1cad..0b414cfe 100644 --- a/core/src/compiler/mod.rs +++ b/core/src/compiler/mod.rs @@ -19,17 +19,17 @@ use crate::ast::*; use crate::bytecode::*; use crate::parser; use crate::reader::LineCol; -use crate::syms::{CallableMetadata, CallableMetadataBuilder, Symbol, SymbolKey, Symbols}; +use crate::syms::{CallableMetadata, CallableMetadataBuilder, SymbolKey, Symbols}; use std::borrow::Cow; use std::collections::HashMap; -#[cfg(test)] -use std::collections::HashSet; use std::io; mod args; pub use args::*; mod exprs; use exprs::{compile_expr, compile_expr_as_type}; +mod symtable; +use symtable::{SymbolPrototype, SymbolsTable}; /// Compilation errors. #[derive(Debug, thiserror::Error)] @@ -41,7 +41,7 @@ pub enum Error { #[error("{0}: Cannot {1} {2} and {3}")] BinaryOpTypeError(LineCol, &'static str, ExprType, ExprType), - #[error("{0}: {} expected {}", .1.name(), .1.syntax())] + #[error("{0}: {} expected {}", .1.name(), .1.syntax().as_deref().unwrap_or("no arguments"))] CallableSyntaxError(LineCol, CallableMetadata), #[error("{0}: Duplicate label {1}")] @@ -111,188 +111,6 @@ impl From for Error { /// Result for compiler return values. pub type Result = std::result::Result; -/// Information about a symbol in the symbols table. -#[derive(Clone)] -enum SymbolPrototype { - /// Information about an array. The integer indicates the number of dimensions in the array. - Array(ExprType, usize), - - /// Information about a callable that's a builtin and requires an upcall to execute. - /// The integer indicates the runtime upcall index of the callable. - BuiltinCallable(CallableMetadata, usize), - - /// Information about a callable. - Callable(CallableMetadata), - - /// Information about a variable. - Variable(ExprType), -} - -/// The symbols table used during compilation. -/// -/// Symbols are represented as a two-layer map: the globals map contains all symbols that are -/// visible by all scopes, and the scope contains all symbols that are only visible within a -/// given scope. -/// -/// The collection of symbols that is visible at any given point in time is thus the union of -/// the global symbols and the symbols in the last scope. -/// -/// There is always at least one scope in the scopes stack: at the program level outside of -/// functions, variables are not global by default and thus they are kept in their own scope. -/// But because we do not support nested function definitions, the scopes "stack" should -/// always have size of one or two. -struct SymbolsTable { - /// Map of global symbol names to their definitions. - globals: HashMap, - - /// Map of local symbol names to their definitions. - scopes: Vec>, -} - -impl Default for SymbolsTable { - fn default() -> Self { - Self { globals: HashMap::default(), scopes: vec![HashMap::default()] } - } -} - -impl From> for SymbolsTable { - fn from(globals: HashMap) -> Self { - Self { globals, scopes: vec![HashMap::default()] } - } -} - -impl From<&Symbols> for SymbolsTable { - fn from(syms: &Symbols) -> Self { - let globals = { - let mut globals = HashMap::default(); - - let callables = syms.callables(); - let mut names = callables.keys().copied().collect::>(); - // This is only necessary for testing really... but may also remove some confusion - // when inspecting the bytecode because it helps keep upcall indexes stable across - // different compilations. - names.sort(); - - for (i, name) in names.into_iter().enumerate() { - let callable = callables.get(&name).unwrap(); - let proto = SymbolPrototype::BuiltinCallable(callable.metadata().clone(), i); - globals.insert(name.clone(), proto); - } - - globals - }; - - let mut scope = HashMap::default(); - for (name, symbol) in syms.locals() { - let proto = match symbol { - Symbol::Array(array) => { - SymbolPrototype::Array(array.subtype(), array.dimensions().len()) - } - Symbol::Callable(_) => { - unreachable!("Callables must only be global"); - } - Symbol::Variable(var) => SymbolPrototype::Variable(var.as_exprtype()), - }; - scope.insert(name.clone(), proto); - } - - Self { globals, scopes: vec![scope] } - } -} - -impl SymbolsTable { - /// Enters a new scope. - fn enter_scope(&mut self) { - self.scopes.push(HashMap::default()); - } - - /// Leaves the current scope. - fn leave_scope(&mut self) { - let last = self.scopes.pop(); - assert!(last.is_some(), "Must have at least one scope to pop"); - assert!(!self.scopes.is_empty(), "Cannot pop the global scope"); - } - - /// Returns true if the symbols table contains `key`. - fn contains_key(&mut self, key: &SymbolKey) -> bool { - self.scopes.last().unwrap().contains_key(key) || self.globals.contains_key(key) - } - - /// Returns the information for the symbol `key` if it exists, otherwise `None`. - fn get(&self, key: &SymbolKey) -> Option<&SymbolPrototype> { - let proto = self.scopes.last().unwrap().get(key); - if proto.is_some() { - return proto; - } - - self.globals.get(key) - } - - /// Inserts the new information `proto` about symbol `key` into the symbols table. - /// The symbol must not yet exist. - fn insert(&mut self, key: SymbolKey, proto: SymbolPrototype) { - debug_assert!(!self.globals.contains_key(&key), "Cannot redefine a symbol"); - let previous = self.scopes.last_mut().unwrap().insert(key, proto); - debug_assert!(previous.is_none(), "Cannot redefine a symbol"); - } - - /// Inserts the builtin callable described by `md` and assigns an upcall index. - /// The symbol must not yet exist. - #[cfg(test)] - fn insert_builtin_callable(&mut self, key: SymbolKey, md: CallableMetadata) { - let next_upcall_index = self - .globals - .values() - .filter(|proto| matches!(proto, SymbolPrototype::BuiltinCallable(..))) - .count(); - - debug_assert!(!self.globals.contains_key(&key), "Cannot redefine a symbol"); - let proto = SymbolPrototype::BuiltinCallable(md, next_upcall_index); - let previous = self.globals.insert(key, proto); - debug_assert!(previous.is_none(), "Cannot redefine a symbol"); - } - - /// Inserts the new information `proto` about symbol `key` into the symbols table. - /// The symbol must not yet exist. - fn insert_global(&mut self, key: SymbolKey, proto: SymbolPrototype) { - let previous = self.globals.insert(key, proto); - debug_assert!(previous.is_none(), "Cannot redefine a symbol"); - } - - /// Removes information about the symbol `key`. - fn remove(&mut self, key: SymbolKey) { - let previous = self.scopes.last_mut().unwrap().remove(&key); - debug_assert!(previous.is_some(), "Cannot unset a non-existing symbol"); - } - - /// Returns a view of the keys in the symbols table. - #[cfg(test)] - fn keys(&self) -> HashSet<&SymbolKey> { - let mut keys = HashSet::default(); - keys.extend(self.globals.keys()); - keys.extend(self.scopes.last().unwrap().keys()); - keys - } - - /// Calculates the list of upcalls in this symbols table in the order in which they were - /// assigned indexes. - fn upcalls(&self) -> Vec { - let mut builtins = self - .globals - .iter() - .filter_map(|(key, proto)| { - if let SymbolPrototype::BuiltinCallable(_md, upcall_index) = proto { - Some((upcall_index, key)) - } else { - None - } - }) - .collect::>(); - builtins.sort_by_key(|(upcall_index, _key)| *upcall_index); - builtins.into_iter().map(|(_upcall_index, key)| key.clone()).collect() - } -} - /// Describes a location in the code needs fixing up after all addresses have been laid out. #[cfg_attr(test, derive(Debug, PartialEq))] enum Fixup { @@ -456,8 +274,8 @@ impl Compiler { /// Compiles an assignment to an array position. fn compile_array_assignment(&mut self, span: ArrayAssignmentSpan) -> Result<()> { let key = SymbolKey::from(span.vref.name()); - let (atype, dims) = match self.symtable.get(&key) { - Some(SymbolPrototype::Array(atype, dims)) => (*atype, *dims), + let ((atype, dims), index) = match self.symtable.get_with_index(&key) { + Some((SymbolPrototype::Array(atype, dims), index)) => ((*atype, *dims), index), Some(_) => { return Err(Error::IndexNonArray(span.vref_pos, span.vref.take_name())); } @@ -479,7 +297,12 @@ impl Compiler { let nargs = span.subscripts.len(); self.compile_array_indices(dims, span.subscripts, span.vref_pos)?; - self.emit(Instruction::ArrayAssignment(key, span.vref_pos, nargs)); + self.emit(Instruction::ArrayAssignment(ArrayIndexISpan { + name: key, + name_pos: span.vref_pos, + index, + nargs, + })); Ok(()) } @@ -488,7 +311,9 @@ impl Compiler { /// /// It's important to always use this function instead of manually emitting `Instruction::Assign` /// instructions to ensure consistent handling of the symbols table. - fn compile_assignment(&mut self, vref: VarRef, vref_pos: LineCol, expr: Expr) -> Result<()> { + /// + /// Returns the index of the assigned variable. + fn compile_assignment(&mut self, vref: VarRef, vref_pos: LineCol, expr: Expr) -> Result { let mut key = SymbolKey::from(&vref.name()); let etype = self.compile_expr(expr)?; @@ -498,16 +323,15 @@ impl Compiler { } } - let vtype = match self.symtable.get(&key) { - Some(SymbolPrototype::Variable(vtype)) => *vtype, + let (vtype, index) = match self.symtable.get_with_index(&key) { + Some((SymbolPrototype::Variable(vtype), index)) => (*vtype, index), Some(_) => return Err(Error::RedefinitionError(vref_pos, key)), None => { // TODO(jmmv): Compile separate Dim instructions for new variables instead of // checking this every time. let key = key.clone(); let vtype = vref.ref_type().unwrap_or(etype); - self.symtable.insert(key, SymbolPrototype::Variable(vtype)); - vtype + (vtype, self.symtable.insert(key, SymbolPrototype::Variable(vtype))) } }; @@ -522,9 +346,9 @@ impl Compiler { } } - self.emit(Instruction::Assign(key)); + self.emit(Instruction::Assign(key, index)); - Ok(()) + Ok(index) } /// Compiles a `FUNCTION` or `SUB` definition. @@ -568,16 +392,18 @@ impl Compiler { return Err(Error::RedefinitionError(span.name_pos, key)); } + let index = if span.shared { + self.symtable.insert_global(key.clone(), SymbolPrototype::Variable(span.vtype)) + } else { + self.symtable.insert(key.clone(), SymbolPrototype::Variable(span.vtype)) + }; + self.emit(Instruction::Dim(DimISpan { - name: key.clone(), + name: key, + index, shared: span.shared, vtype: span.vtype, })); - if span.shared { - self.symtable.insert_global(key, SymbolPrototype::Variable(span.vtype)); - } else { - self.symtable.insert(key, SymbolPrototype::Variable(span.vtype)); - } Ok(()) } @@ -652,18 +478,25 @@ impl Compiler { let key = SymbolKey::from(span.iter.name()); let skip_pc = self.emit(Instruction::Nop); - let iter_key = SymbolKey::from(span.iter.name()); - if self.symtable.get(&key).is_none() { - self.emit(Instruction::Dim(DimISpan { - name: key, - shared: false, - vtype: ExprType::Double, - })); - self.symtable.insert(iter_key.clone(), SymbolPrototype::Variable(ExprType::Double)); - } + let index = match self.symtable.get_with_index(&key) { + Some((_, index)) => index, + None => { + let index = self + .symtable + .insert(key.clone(), SymbolPrototype::Variable(ExprType::Double)); + self.emit(Instruction::Dim(DimISpan { + name: key.clone(), + index, + shared: false, + vtype: ExprType::Double, + })); + index + } + }; self.instrs[skip_pc] = Instruction::JumpIfDefined(JumpIfDefinedISpan { - var: iter_key, + var: key, + index, addr: self.instrs.len(), }); } @@ -802,7 +635,7 @@ impl Compiler { self.selects += 1; let test_vref = VarRef::new(Compiler::select_test_var_name(self.selects), None); - self.compile_assignment(test_vref.clone(), span.expr.start_pos(), span.expr)?; + let index = self.compile_assignment(test_vref.clone(), span.expr.start_pos(), span.expr)?; let mut iter = span.cases.into_iter(); let mut next = iter.next(); @@ -835,7 +668,11 @@ impl Compiler { } let test_key = SymbolKey::from(test_vref.name()); - self.emit(Instruction::Unset(UnsetISpan { name: test_key.clone(), pos: span.end_pos })); + self.emit(Instruction::Unset(UnsetISpan { + name: test_key.clone(), + pos: span.end_pos, + index, + })); self.symtable.remove(test_key); Ok(()) @@ -895,15 +732,15 @@ impl Compiler { Statement::Call(span) => { let key = SymbolKey::from(&span.vref.name()); - let (md, upcall_index) = match self.symtable.get(&key) { - Some(SymbolPrototype::BuiltinCallable(md, upcall_index)) => { + let (md, upcall_index) = match self.symtable.get_with_index(&key) { + Some((SymbolPrototype::BuiltinCallable(md), upcall_index)) => { if md.is_function() { return Err(Error::NotACommand(span.vref_pos, span.vref)); } - (md.clone(), Some(*upcall_index)) + (md.clone(), Some(upcall_index)) } - Some(SymbolPrototype::Callable(md)) => { + Some((SymbolPrototype::Callable(md), _index)) => { if md.is_function() { return Err(Error::NotACommand(span.vref_pos, span.vref)); } @@ -961,20 +798,22 @@ impl Compiler { for arg in span.dimensions.into_iter().rev() { self.compile_expr_as_type(arg, ExprType::Integer)?; } + + let index = if span.shared { + self.symtable + .insert_global(key.clone(), SymbolPrototype::Array(span.subtype, nargs)) + } else { + self.symtable.insert(key.clone(), SymbolPrototype::Array(span.subtype, nargs)) + }; self.emit(Instruction::DimArray(DimArrayISpan { - name: key.clone(), + name: key, name_pos: span.name_pos, + index, shared: span.shared, dimensions: nargs, subtype: span.subtype, subtype_pos: span.subtype_pos, })); - - if span.shared { - self.symtable.insert_global(key, SymbolPrototype::Array(span.subtype, nargs)); - } else { - self.symtable.insert(key, SymbolPrototype::Array(span.subtype, nargs)); - } } Statement::Do(span) => { @@ -1119,19 +958,22 @@ impl Compiler { self.emit(Instruction::EnterScope); self.symtable.enter_scope(); + let index = self + .symtable + .insert(return_value.clone(), SymbolPrototype::Variable(return_type)); self.emit(Instruction::Dim(DimISpan { name: return_value.clone(), + index, shared: false, vtype: return_type, })); - self.symtable - .insert(return_value.clone(), SymbolPrototype::Variable(return_type)); for param in span.params { let key = SymbolKey::from(param.name()); let ptype = param.ref_type().unwrap_or(ExprType::Integer); - self.emit(Instruction::Assign(key.clone())); - self.symtable.insert(key, SymbolPrototype::Variable(ptype)); + let index = + self.symtable.insert(key.clone(), SymbolPrototype::Variable(ptype)); + self.emit(Instruction::Assign(key, index)); } debug_assert!(self.current_callable.is_none(), "Callables cannot be nested"); @@ -1145,7 +987,11 @@ impl Compiler { ExprType::Integer => Instruction::LoadInteger, ExprType::Text => Instruction::LoadString, }; - let epilogue_pc = self.emit(load_inst(return_value.clone(), span.end_pos)); + let epilogue_pc = self.emit(load_inst(LoadISpan { + name: return_value.clone(), + pos: span.end_pos, + index, + })); self.emit(Instruction::LeaveScope); self.symtable.leave_scope(); @@ -1164,8 +1010,9 @@ impl Compiler { for param in span.params { let key = SymbolKey::from(param.name()); let ptype = param.ref_type().unwrap_or(ExprType::Integer); - self.emit(Instruction::Assign(key.clone())); - self.symtable.insert(key, SymbolPrototype::Variable(ptype)); + let index = + self.symtable.insert(key.clone(), SymbolPrototype::Variable(ptype)); + self.emit(Instruction::Assign(key, index)); } debug_assert!(self.current_callable.is_none(), "Callables cannot be nested"); @@ -1443,12 +1290,34 @@ mod tests { .parse("foo(3, 4 + i, i) = 5") .compile() .expect_instr(0, Instruction::PushInteger(5, lc(1, 20))) - .expect_instr(1, Instruction::LoadInteger(SymbolKey::from("i"), lc(1, 15))) + .expect_instr( + 1, + Instruction::LoadInteger(LoadISpan { + name: SymbolKey::from("i"), + pos: lc(1, 15), + index: 1, + }), + ) .expect_instr(2, Instruction::PushInteger(4, lc(1, 8))) - .expect_instr(3, Instruction::LoadInteger(SymbolKey::from("i"), lc(1, 12))) + .expect_instr( + 3, + Instruction::LoadInteger(LoadISpan { + name: SymbolKey::from("i"), + pos: lc(1, 12), + index: 1, + }), + ) .expect_instr(4, Instruction::AddIntegers(lc(1, 10))) .expect_instr(5, Instruction::PushInteger(3, lc(1, 5))) - .expect_instr(6, Instruction::ArrayAssignment(SymbolKey::from("foo"), lc(1, 1), 3)) + .expect_instr( + 6, + Instruction::ArrayAssignment(ArrayIndexISpan { + name: SymbolKey::from("foo"), + name_pos: lc(1, 1), + index: 0, + nargs: 3, + }), + ) .check(); } @@ -1460,7 +1329,15 @@ mod tests { .compile() .expect_instr(0, Instruction::PushInteger(1, lc(1, 9))) .expect_instr(1, Instruction::PushInteger(0, lc(1, 4))) - .expect_instr(2, Instruction::ArrayAssignment(SymbolKey::from("a"), lc(1, 1), 1)) + .expect_instr( + 2, + Instruction::ArrayAssignment(ArrayIndexISpan { + name: SymbolKey::from("a"), + name_pos: lc(1, 1), + index: 0, + nargs: 1, + }), + ) .check(); } @@ -1473,7 +1350,15 @@ mod tests { .expect_instr(0, Instruction::PushInteger(1, lc(1, 10))) .expect_instr(1, Instruction::PushDouble(1.2, lc(1, 3))) .expect_instr(2, Instruction::DoubleToInteger) - .expect_instr(3, Instruction::ArrayAssignment(SymbolKey::from("a"), lc(1, 1), 1)) + .expect_instr( + 3, + Instruction::ArrayAssignment(ArrayIndexISpan { + name: SymbolKey::from("a"), + name_pos: lc(1, 1), + index: 0, + nargs: 1, + }), + ) .check(); } @@ -1504,10 +1389,25 @@ mod tests { .define("d", SymbolPrototype::Variable(ExprType::Double)) .parse("a(3) = d") .compile() - .expect_instr(0, Instruction::LoadDouble(SymbolKey::from("d"), lc(1, 8))) + .expect_instr( + 0, + Instruction::LoadDouble(LoadISpan { + name: SymbolKey::from("d"), + pos: lc(1, 8), + index: 1, + }), + ) .expect_instr(1, Instruction::DoubleToInteger) .expect_instr(2, Instruction::PushInteger(3, lc(1, 3))) - .expect_instr(3, Instruction::ArrayAssignment(SymbolKey::from("a"), lc(1, 1), 1)) + .expect_instr( + 3, + Instruction::ArrayAssignment(ArrayIndexISpan { + name: SymbolKey::from("a"), + name_pos: lc(1, 1), + index: 0, + nargs: 1, + }), + ) .check(); } @@ -1518,10 +1418,25 @@ mod tests { .define("i", SymbolPrototype::Variable(ExprType::Integer)) .parse("a(3) = i") .compile() - .expect_instr(0, Instruction::LoadInteger(SymbolKey::from("i"), lc(1, 8))) + .expect_instr( + 0, + Instruction::LoadInteger(LoadISpan { + name: SymbolKey::from("i"), + pos: lc(1, 8), + index: 1, + }), + ) .expect_instr(1, Instruction::IntegerToDouble) .expect_instr(2, Instruction::PushInteger(3, lc(1, 3))) - .expect_instr(3, Instruction::ArrayAssignment(SymbolKey::from("a"), lc(1, 1), 1)) + .expect_instr( + 3, + Instruction::ArrayAssignment(ArrayIndexISpan { + name: SymbolKey::from("a"), + name_pos: lc(1, 1), + index: 0, + nargs: 1, + }), + ) .check(); } @@ -1572,7 +1487,7 @@ mod tests { .parse("foo = 5") .compile() .expect_instr(0, Instruction::PushInteger(5, lc(1, 7))) - .expect_instr(1, Instruction::Assign(SymbolKey::from("foo"))) + .expect_instr(1, Instruction::Assign(SymbolKey::from("foo"), 0)) .check(); } @@ -1582,8 +1497,15 @@ mod tests { .define("i", SymbolPrototype::Variable(ExprType::Integer)) .parse("foo = i") .compile() - .expect_instr(0, Instruction::LoadInteger(SymbolKey::from("i"), lc(1, 7))) - .expect_instr(1, Instruction::Assign(SymbolKey::from("foo"))) + .expect_instr( + 0, + Instruction::LoadInteger(LoadISpan { + name: SymbolKey::from("i"), + pos: lc(1, 7), + index: 0, + }), + ) + .expect_instr(1, Instruction::Assign(SymbolKey::from("foo"), 1)) .check(); } @@ -1593,7 +1515,7 @@ mod tests { .parse("foo = 2.3") .compile() .expect_instr(0, Instruction::PushDouble(2.3, lc(1, 7))) - .expect_instr(1, Instruction::Assign(SymbolKey::from("foo"))) + .expect_instr(1, Instruction::Assign(SymbolKey::from("foo"), 0)) .expect_symtable(SymbolKey::from("foo"), SymbolPrototype::Variable(ExprType::Double)) .check(); } @@ -1605,7 +1527,7 @@ mod tests { .compile() .expect_instr(0, Instruction::PushInteger(2, lc(1, 8))) .expect_instr(1, Instruction::IntegerToDouble) - .expect_instr(2, Instruction::Assign(SymbolKey::from("foo"))) + .expect_instr(2, Instruction::Assign(SymbolKey::from("foo"), 0)) .expect_symtable(SymbolKey::from("foo"), SymbolPrototype::Variable(ExprType::Double)) .check(); } @@ -1714,6 +1636,7 @@ mod tests { 0, Instruction::Dim(DimISpan { name: SymbolKey::from("var"), + index: 0, shared: false, vtype: ExprType::Boolean, }), @@ -1726,6 +1649,7 @@ mod tests { 0, Instruction::Dim(DimISpan { name: SymbolKey::from("var"), + index: 0, shared: false, vtype: ExprType::Double, }), @@ -1738,6 +1662,7 @@ mod tests { 0, Instruction::Dim(DimISpan { name: SymbolKey::from("var"), + index: 0, shared: false, vtype: ExprType::Integer, }), @@ -1750,6 +1675,7 @@ mod tests { 0, Instruction::Dim(DimISpan { name: SymbolKey::from("var"), + index: 0, shared: false, vtype: ExprType::Text, }), @@ -1801,6 +1727,7 @@ mod tests { 0, Instruction::Dim(DimISpan { name: SymbolKey::from("var"), + index: 0, shared: true, vtype: ExprType::Boolean, }), @@ -1834,6 +1761,7 @@ mod tests { Instruction::DimArray(DimArrayISpan { name: SymbolKey::from("var"), name_pos: lc(1, 5), + index: 0, shared: false, dimensions: 1, subtype: ExprType::Integer, @@ -1852,12 +1780,20 @@ mod tests { .expect_instr(0, Instruction::PushInteger(3, lc(1, 12))) .expect_instr(1, Instruction::PushInteger(4, lc(1, 16))) .expect_instr(2, Instruction::AddIntegers(lc(1, 14))) - .expect_instr(3, Instruction::LoadInteger(SymbolKey::from("i"), lc(1, 9))) + .expect_instr( + 3, + Instruction::LoadInteger(LoadISpan { + name: SymbolKey::from("i"), + pos: lc(1, 9), + index: 0, + }), + ) .expect_instr( 4, Instruction::DimArray(DimArrayISpan { name: SymbolKey::from("var"), name_pos: lc(1, 5), + index: 1, shared: false, dimensions: 2, subtype: ExprType::Integer, @@ -1879,6 +1815,7 @@ mod tests { Instruction::DimArray(DimArrayISpan { name: SymbolKey::from("var"), name_pos: lc(1, 5), + index: 0, shared: false, dimensions: 1, subtype: ExprType::Integer, @@ -1908,6 +1845,7 @@ mod tests { Instruction::DimArray(DimArrayISpan { name: SymbolKey::from("var"), name_pos: lc(1, 12), + index: 0, shared: true, dimensions: 1, subtype: ExprType::Integer, @@ -2004,7 +1942,14 @@ mod tests { .parse("END 2 + i") .compile() .expect_instr(0, Instruction::PushInteger(2, lc(1, 5))) - .expect_instr(1, Instruction::LoadInteger(SymbolKey::from("i"), lc(1, 9))) + .expect_instr( + 1, + Instruction::LoadInteger(LoadISpan { + name: SymbolKey::from("i"), + pos: lc(1, 9), + index: 0, + }), + ) .expect_instr(2, Instruction::AddIntegers(lc(1, 7))) .expect_instr(3, Instruction::End(true)) .check(); @@ -2016,7 +1961,14 @@ mod tests { .define("i", SymbolPrototype::Variable(ExprType::Integer)) .parse("END i") .compile() - .expect_instr(0, Instruction::LoadInteger(SymbolKey::from("i"), lc(1, 5))) + .expect_instr( + 0, + Instruction::LoadInteger(LoadISpan { + name: SymbolKey::from("i"), + pos: lc(1, 5), + index: 0, + }), + ) .expect_instr(1, Instruction::End(true)) .check(); } @@ -2132,16 +2084,30 @@ mod tests { .parse("FOR i = 1 to 10\nEXIT FOR\nNEXT") .compile() .expect_instr(0, Instruction::PushInteger(1, lc(1, 9))) - .expect_instr(1, Instruction::Assign(SymbolKey::from("i"))) - .expect_instr(2, Instruction::LoadInteger(SymbolKey::from("i"), lc(1, 5))) + .expect_instr(1, Instruction::Assign(SymbolKey::from("i"), 0)) + .expect_instr( + 2, + Instruction::LoadInteger(LoadISpan { + name: SymbolKey::from("i"), + pos: lc(1, 5), + index: 0, + }), + ) .expect_instr(3, Instruction::PushInteger(10, lc(1, 14))) .expect_instr(4, Instruction::LessEqualIntegers(lc(1, 11))) .expect_instr(5, Instruction::JumpIfNotTrue(12)) .expect_instr(6, Instruction::Jump(JumpISpan { addr: 12 })) - .expect_instr(7, Instruction::LoadInteger(SymbolKey::from("i"), lc(1, 5))) + .expect_instr( + 7, + Instruction::LoadInteger(LoadISpan { + name: SymbolKey::from("i"), + pos: lc(1, 5), + index: 0, + }), + ) .expect_instr(8, Instruction::PushInteger(1, lc(1, 16))) .expect_instr(9, Instruction::AddIntegers(lc(1, 11))) - .expect_instr(10, Instruction::Assign(SymbolKey::from("i"))) + .expect_instr(10, Instruction::Assign(SymbolKey::from("i"), 0)) .expect_instr(11, Instruction::Jump(JumpISpan { addr: 2 })) .check(); } @@ -2152,30 +2118,58 @@ mod tests { .parse("FOR i = 1 to 10\nFOR j = 2 to 20\nEXIT FOR\nNEXT\nEXIT FOR\nNEXT") .compile() .expect_instr(0, Instruction::PushInteger(1, lc(1, 9))) - .expect_instr(1, Instruction::Assign(SymbolKey::from("i"))) - .expect_instr(2, Instruction::LoadInteger(SymbolKey::from("i"), lc(1, 5))) + .expect_instr(1, Instruction::Assign(SymbolKey::from("i"), 0)) + .expect_instr( + 2, + Instruction::LoadInteger(LoadISpan { + name: SymbolKey::from("i"), + pos: lc(1, 5), + index: 0, + }), + ) .expect_instr(3, Instruction::PushInteger(10, lc(1, 14))) .expect_instr(4, Instruction::LessEqualIntegers(lc(1, 11))) .expect_instr(5, Instruction::JumpIfNotTrue(24)) // Begin nested for loop. .expect_instr(6, Instruction::PushInteger(2, lc(2, 9))) - .expect_instr(7, Instruction::Assign(SymbolKey::from("j"))) - .expect_instr(8, Instruction::LoadInteger(SymbolKey::from("j"), lc(2, 5))) + .expect_instr(7, Instruction::Assign(SymbolKey::from("j"), 1)) + .expect_instr( + 8, + Instruction::LoadInteger(LoadISpan { + name: SymbolKey::from("j"), + pos: lc(2, 5), + index: 1, + }), + ) .expect_instr(9, Instruction::PushInteger(20, lc(2, 14))) .expect_instr(10, Instruction::LessEqualIntegers(lc(2, 11))) .expect_instr(11, Instruction::JumpIfNotTrue(18)) .expect_instr(12, Instruction::Jump(JumpISpan { addr: 18 })) // Exit for. - .expect_instr(13, Instruction::LoadInteger(SymbolKey::from("j"), lc(2, 5))) + .expect_instr( + 13, + Instruction::LoadInteger(LoadISpan { + name: SymbolKey::from("j"), + pos: lc(2, 5), + index: 1, + }), + ) .expect_instr(14, Instruction::PushInteger(1, lc(2, 16))) .expect_instr(15, Instruction::AddIntegers(lc(2, 11))) - .expect_instr(16, Instruction::Assign(SymbolKey::from("j"))) + .expect_instr(16, Instruction::Assign(SymbolKey::from("j"), 1)) .expect_instr(17, Instruction::Jump(JumpISpan { addr: 8 })) // Begin nested for loop. .expect_instr(18, Instruction::Jump(JumpISpan { addr: 24 })) // Exit for. - .expect_instr(19, Instruction::LoadInteger(SymbolKey::from("i"), lc(1, 5))) + .expect_instr( + 19, + Instruction::LoadInteger(LoadISpan { + name: SymbolKey::from("i"), + pos: lc(1, 5), + index: 0, + }), + ) .expect_instr(20, Instruction::PushInteger(1, lc(1, 16))) .expect_instr(21, Instruction::AddIntegers(lc(1, 11))) - .expect_instr(22, Instruction::Assign(SymbolKey::from("i"))) + .expect_instr(22, Instruction::Assign(SymbolKey::from("i"), 0)) .expect_instr(23, Instruction::Jump(JumpISpan { addr: 2 })) .check(); } @@ -2202,8 +2196,15 @@ mod tests { .compile() // Begin nested for loop. .expect_instr(0, Instruction::PushInteger(1, lc(2, 9))) - .expect_instr(1, Instruction::Assign(SymbolKey::from("i"))) - .expect_instr(2, Instruction::LoadInteger(SymbolKey::from("i"), lc(2, 5))) + .expect_instr(1, Instruction::Assign(SymbolKey::from("i"), 0)) + .expect_instr( + 2, + Instruction::LoadInteger(LoadISpan { + name: SymbolKey::from("i"), + pos: lc(2, 5), + index: 0, + }), + ) .expect_instr(3, Instruction::PushInteger(10, lc(2, 14))) .expect_instr(4, Instruction::LessEqualIntegers(lc(2, 11))) .expect_instr(5, Instruction::JumpIfNotTrue(14)) @@ -2212,10 +2213,17 @@ mod tests { .expect_instr(7, Instruction::Jump(JumpISpan { addr: 6 })) // End nested do loop. .expect_instr(8, Instruction::Jump(JumpISpan { addr: 14 })) // Exit for. - .expect_instr(9, Instruction::LoadInteger(SymbolKey::from("i"), lc(2, 5))) + .expect_instr( + 9, + Instruction::LoadInteger(LoadISpan { + name: SymbolKey::from("i"), + pos: lc(2, 5), + index: 0, + }), + ) .expect_instr(10, Instruction::PushInteger(1, lc(2, 16))) .expect_instr(11, Instruction::AddIntegers(lc(2, 11))) - .expect_instr(12, Instruction::Assign(SymbolKey::from("i"))) + .expect_instr(12, Instruction::Assign(SymbolKey::from("i"), 0)) .expect_instr(13, Instruction::Jump(JumpISpan { addr: 2 })) // End nested for loop. .expect_instr(14, Instruction::Jump(JumpISpan { addr: 0 })) @@ -2228,17 +2236,31 @@ mod tests { .parse("FOR iter = 1 TO 5: a = FALSE: NEXT") .compile() .expect_instr(0, Instruction::PushInteger(1, lc(1, 12))) - .expect_instr(1, Instruction::Assign(SymbolKey::from("iter"))) - .expect_instr(2, Instruction::LoadInteger(SymbolKey::from("iter"), lc(1, 5))) + .expect_instr(1, Instruction::Assign(SymbolKey::from("iter"), 0)) + .expect_instr( + 2, + Instruction::LoadInteger(LoadISpan { + name: SymbolKey::from("iter"), + pos: lc(1, 5), + index: 0, + }), + ) .expect_instr(3, Instruction::PushInteger(5, lc(1, 17))) .expect_instr(4, Instruction::LessEqualIntegers(lc(1, 14))) .expect_instr(5, Instruction::JumpIfNotTrue(13)) .expect_instr(6, Instruction::PushBoolean(false, lc(1, 24))) - .expect_instr(7, Instruction::Assign(SymbolKey::from("a"))) - .expect_instr(8, Instruction::LoadInteger(SymbolKey::from("iter"), lc(1, 5))) + .expect_instr(7, Instruction::Assign(SymbolKey::from("a"), 1)) + .expect_instr( + 8, + Instruction::LoadInteger(LoadISpan { + name: SymbolKey::from("iter"), + pos: lc(1, 5), + index: 0, + }), + ) .expect_instr(9, Instruction::PushInteger(1, lc(1, 18))) .expect_instr(10, Instruction::AddIntegers(lc(1, 14))) - .expect_instr(11, Instruction::Assign(SymbolKey::from("iter"))) + .expect_instr(11, Instruction::Assign(SymbolKey::from("iter"), 0)) .expect_instr(12, Instruction::Jump(JumpISpan { addr: 2 })) .check(); } @@ -2250,18 +2272,46 @@ mod tests { .define("j", SymbolPrototype::Variable(ExprType::Integer)) .parse("FOR iter = i TO j: a = FALSE: NEXT") .compile() - .expect_instr(0, Instruction::LoadInteger(SymbolKey::from("i"), lc(1, 12))) - .expect_instr(1, Instruction::Assign(SymbolKey::from("iter"))) - .expect_instr(2, Instruction::LoadInteger(SymbolKey::from("iter"), lc(1, 5))) - .expect_instr(3, Instruction::LoadInteger(SymbolKey::from("j"), lc(1, 17))) + .expect_instr( + 0, + Instruction::LoadInteger(LoadISpan { + name: SymbolKey::from("i"), + pos: lc(1, 12), + index: 0, + }), + ) + .expect_instr(1, Instruction::Assign(SymbolKey::from("iter"), 2)) + .expect_instr( + 2, + Instruction::LoadInteger(LoadISpan { + name: SymbolKey::from("iter"), + pos: lc(1, 5), + index: 2, + }), + ) + .expect_instr( + 3, + Instruction::LoadInteger(LoadISpan { + name: SymbolKey::from("j"), + pos: lc(1, 17), + index: 1, + }), + ) .expect_instr(4, Instruction::LessEqualIntegers(lc(1, 14))) .expect_instr(5, Instruction::JumpIfNotTrue(13)) .expect_instr(6, Instruction::PushBoolean(false, lc(1, 24))) - .expect_instr(7, Instruction::Assign(SymbolKey::from("a"))) - .expect_instr(8, Instruction::LoadInteger(SymbolKey::from("iter"), lc(1, 5))) + .expect_instr(7, Instruction::Assign(SymbolKey::from("a"), 3)) + .expect_instr( + 8, + Instruction::LoadInteger(LoadISpan { + name: SymbolKey::from("iter"), + pos: lc(1, 5), + index: 2, + }), + ) .expect_instr(9, Instruction::PushInteger(1, lc(1, 18))) .expect_instr(10, Instruction::AddIntegers(lc(1, 14))) - .expect_instr(11, Instruction::Assign(SymbolKey::from("iter"))) + .expect_instr(11, Instruction::Assign(SymbolKey::from("iter"), 2)) .expect_instr(12, Instruction::Jump(JumpISpan { addr: 2 })) .check(); } @@ -2273,22 +2323,50 @@ mod tests { .define("j", SymbolPrototype::Variable(ExprType::Integer)) .parse("FOR iter = (i + 1) TO (2 + j): a = FALSE: NEXT") .compile() - .expect_instr(0, Instruction::LoadInteger(SymbolKey::from("i"), lc(1, 13))) + .expect_instr( + 0, + Instruction::LoadInteger(LoadISpan { + name: SymbolKey::from("i"), + pos: lc(1, 13), + index: 0, + }), + ) .expect_instr(1, Instruction::PushInteger(1, lc(1, 17))) .expect_instr(2, Instruction::AddIntegers(lc(1, 15))) - .expect_instr(3, Instruction::Assign(SymbolKey::from("iter"))) - .expect_instr(4, Instruction::LoadInteger(SymbolKey::from("iter"), lc(1, 5))) + .expect_instr(3, Instruction::Assign(SymbolKey::from("iter"), 2)) + .expect_instr( + 4, + Instruction::LoadInteger(LoadISpan { + name: SymbolKey::from("iter"), + pos: lc(1, 5), + index: 2, + }), + ) .expect_instr(5, Instruction::PushInteger(2, lc(1, 24))) - .expect_instr(6, Instruction::LoadInteger(SymbolKey::from("j"), lc(1, 28))) + .expect_instr( + 6, + Instruction::LoadInteger(LoadISpan { + name: SymbolKey::from("j"), + pos: lc(1, 28), + index: 1, + }), + ) .expect_instr(7, Instruction::AddIntegers(lc(1, 26))) .expect_instr(8, Instruction::LessEqualIntegers(lc(1, 20))) .expect_instr(9, Instruction::JumpIfNotTrue(17)) .expect_instr(10, Instruction::PushBoolean(false, lc(1, 36))) - .expect_instr(11, Instruction::Assign(SymbolKey::from("a"))) - .expect_instr(12, Instruction::LoadInteger(SymbolKey::from("iter"), lc(1, 5))) + .expect_instr(11, Instruction::Assign(SymbolKey::from("a"), 3)) + .expect_instr( + 12, + Instruction::LoadInteger(LoadISpan { + name: SymbolKey::from("iter"), + pos: lc(1, 5), + index: 2, + }), + ) .expect_instr(13, Instruction::PushInteger(1, lc(1, 30))) .expect_instr(14, Instruction::AddIntegers(lc(1, 20))) - .expect_instr(15, Instruction::Assign(SymbolKey::from("iter"))) + .expect_instr(15, Instruction::Assign(SymbolKey::from("iter"), 2)) .expect_instr(16, Instruction::Jump(JumpISpan { addr: 4 })) .check(); } @@ -2302,6 +2380,7 @@ mod tests { 0, Instruction::JumpIfDefined(JumpIfDefinedISpan { var: SymbolKey::from("iter"), + index: 0, addr: 2, }), ) @@ -2309,22 +2388,37 @@ mod tests { 1, Instruction::Dim(DimISpan { name: SymbolKey::from("iter"), + index: 0, shared: false, vtype: ExprType::Double, }), ) .expect_instr(2, Instruction::PushInteger(0, lc(1, 12))) .expect_instr(3, Instruction::IntegerToDouble) - .expect_instr(4, Instruction::Assign(SymbolKey::from("iter"))) - .expect_instr(5, Instruction::LoadDouble(SymbolKey::from("iter"), lc(1, 5))) + .expect_instr(4, Instruction::Assign(SymbolKey::from("iter"), 0)) + .expect_instr( + 5, + Instruction::LoadDouble(LoadISpan { + name: SymbolKey::from("iter"), + pos: lc(1, 5), + index: 0, + }), + ) .expect_instr(6, Instruction::PushInteger(2, lc(1, 17))) .expect_instr(7, Instruction::IntegerToDouble) .expect_instr(8, Instruction::LessEqualDoubles(lc(1, 14))) .expect_instr(9, Instruction::JumpIfNotTrue(15)) - .expect_instr(10, Instruction::LoadDouble(SymbolKey::from("iter"), lc(1, 5))) + .expect_instr( + 10, + Instruction::LoadDouble(LoadISpan { + name: SymbolKey::from("iter"), + pos: lc(1, 5), + index: 0, + }), + ) .expect_instr(11, Instruction::PushDouble(0.1, lc(1, 24))) .expect_instr(12, Instruction::AddDoubles(lc(1, 14))) - .expect_instr(13, Instruction::Assign(SymbolKey::from("iter"))) + .expect_instr(13, Instruction::Assign(SymbolKey::from("iter"), 0)) .expect_instr(14, Instruction::Jump(JumpISpan { addr: 5 })) .check(); } @@ -2340,13 +2434,21 @@ mod tests { 2, Instruction::Dim(DimISpan { name: SymbolKey::from("0return_foo"), + index: 0, shared: false, vtype: ExprType::Integer, }), ) .expect_instr(3, Instruction::PushInteger(3, lc(1, 19))) - .expect_instr(4, Instruction::Assign(SymbolKey::from("a"))) - .expect_instr(5, Instruction::LoadInteger(SymbolKey::from("0return_foo"), lc(1, 22))) + .expect_instr(4, Instruction::Assign(SymbolKey::from("a"), 1)) + .expect_instr( + 5, + Instruction::LoadInteger(LoadISpan { + name: SymbolKey::from("0return_foo"), + pos: lc(1, 22), + index: 0, + }), + ) .expect_instr(6, Instruction::LeaveScope) .expect_instr(7, Instruction::Return(lc(1, 22))) .expect_symtable( @@ -2366,20 +2468,28 @@ mod tests { .parse("before = 1: FUNCTION foo: END FUNCTION: after = 2") .compile() .expect_instr(0, Instruction::PushInteger(1, lc(1, 10))) - .expect_instr(1, Instruction::Assign(SymbolKey::from("before"))) + .expect_instr(1, Instruction::Assign(SymbolKey::from("before"), 0)) .expect_instr(2, Instruction::PushInteger(2, lc(1, 49))) - .expect_instr(3, Instruction::Assign(SymbolKey::from("after"))) + .expect_instr(3, Instruction::Assign(SymbolKey::from("after"), 1)) .expect_instr(4, Instruction::Jump(JumpISpan { addr: 10 })) .expect_instr(5, Instruction::EnterScope) .expect_instr( 6, Instruction::Dim(DimISpan { name: SymbolKey::from("0return_foo"), + index: 0, shared: false, vtype: ExprType::Integer, }), ) - .expect_instr(7, Instruction::LoadInteger(SymbolKey::from("0return_foo"), lc(1, 27))) + .expect_instr( + 7, + Instruction::LoadInteger(LoadISpan { + name: SymbolKey::from("0return_foo"), + pos: lc(1, 27), + index: 0, + }), + ) .expect_instr(8, Instruction::LeaveScope) .expect_instr(9, Instruction::Return(lc(1, 27))) .expect_symtable( @@ -2404,16 +2514,24 @@ mod tests { 2, Instruction::Dim(DimISpan { name: SymbolKey::from("0return_foo"), + index: 0, shared: false, vtype: ExprType::Integer, }), ) .expect_instr(3, Instruction::PushInteger(3, lc(1, 19))) - .expect_instr(4, Instruction::Assign(SymbolKey::from("a"))) + .expect_instr(4, Instruction::Assign(SymbolKey::from("a"), 1)) .expect_instr(5, Instruction::Jump(JumpISpan { addr: 8 })) .expect_instr(6, Instruction::PushInteger(4, lc(1, 41))) - .expect_instr(7, Instruction::Assign(SymbolKey::from("a"))) - .expect_instr(8, Instruction::LoadInteger(SymbolKey::from("0return_foo"), lc(1, 44))) + .expect_instr(7, Instruction::Assign(SymbolKey::from("a"), 1)) + .expect_instr( + 8, + Instruction::LoadInteger(LoadISpan { + name: SymbolKey::from("0return_foo"), + pos: lc(1, 44), + index: 0, + }), + ) .expect_instr(9, Instruction::LeaveScope) .expect_instr(10, Instruction::Return(lc(1, 44))) .expect_symtable( @@ -2480,10 +2598,10 @@ mod tests { .expect_instr(0, Instruction::Jump(JumpISpan { addr: 9 })) .expect_instr(1, Instruction::EnterScope) .expect_instr(2, Instruction::PushInteger(3, lc(1, 14))) - .expect_instr(3, Instruction::Assign(SymbolKey::from("a"))) + .expect_instr(3, Instruction::Assign(SymbolKey::from("a"), 0)) .expect_instr(4, Instruction::Jump(JumpISpan { addr: 7 })) .expect_instr(5, Instruction::PushInteger(4, lc(1, 31))) - .expect_instr(6, Instruction::Assign(SymbolKey::from("a"))) + .expect_instr(6, Instruction::Assign(SymbolKey::from("a"), 0)) .expect_instr(7, Instruction::LeaveScope) .expect_instr(8, Instruction::Return(lc(1, 34))) .expect_symtable( @@ -2694,7 +2812,7 @@ mod tests { .parse(&format!("SELECT CASE 5\nCASE {}\nFOO\nEND SELECT", guards)) .compile() .expect_instr(0, Instruction::PushInteger(5, lc(1, 13))) - .expect_instr(1, Instruction::Assign(SymbolKey::from("0select1"))); + .expect_instr(1, Instruction::Assign(SymbolKey::from("0select1"), 0)); let mut n = 2; for instr in exp_expr_instrs { t = t.expect_instr(n, instr); @@ -2712,7 +2830,11 @@ mod tests { ) .expect_instr( n + 2, - Instruction::Unset(UnsetISpan { name: SymbolKey::from("0select1"), pos: lc(4, 1) }), + Instruction::Unset(UnsetISpan { + name: SymbolKey::from("0select1"), + pos: lc(4, 1), + index: 0, + }), ) .check(); } @@ -2722,7 +2844,11 @@ mod tests { do_compile_case_guard_test( "1 + 2", vec![ - Instruction::LoadInteger(SymbolKey::from("0select1"), lc(2, 6)), + Instruction::LoadInteger(LoadISpan { + name: SymbolKey::from("0select1"), + pos: lc(2, 6), + index: 0, + }), Instruction::PushInteger(1, lc(2, 6)), Instruction::PushInteger(2, lc(2, 10)), Instruction::AddIntegers(lc(2, 8)), @@ -2736,7 +2862,11 @@ mod tests { do_compile_case_guard_test( "IS = 9 + 8", vec![ - Instruction::LoadInteger(SymbolKey::from("0select1"), lc(2, 11)), + Instruction::LoadInteger(LoadISpan { + name: SymbolKey::from("0select1"), + pos: lc(2, 11), + index: 0, + }), Instruction::PushInteger(9, lc(2, 11)), Instruction::PushInteger(8, lc(2, 15)), Instruction::AddIntegers(lc(2, 13)), @@ -2747,7 +2877,11 @@ mod tests { do_compile_case_guard_test( "IS <> 9", vec![ - Instruction::LoadInteger(SymbolKey::from("0select1"), lc(2, 12)), + Instruction::LoadInteger(LoadISpan { + name: SymbolKey::from("0select1"), + pos: lc(2, 12), + index: 0, + }), Instruction::PushInteger(9, lc(2, 12)), Instruction::NotEqualIntegers(lc(2, 12)), ], @@ -2756,7 +2890,11 @@ mod tests { do_compile_case_guard_test( "IS < 9", vec![ - Instruction::LoadInteger(SymbolKey::from("0select1"), lc(2, 11)), + Instruction::LoadInteger(LoadISpan { + name: SymbolKey::from("0select1"), + pos: lc(2, 11), + index: 0, + }), Instruction::PushInteger(9, lc(2, 11)), Instruction::LessIntegers(lc(2, 11)), ], @@ -2765,7 +2903,11 @@ mod tests { do_compile_case_guard_test( "IS <= 9", vec![ - Instruction::LoadInteger(SymbolKey::from("0select1"), lc(2, 12)), + Instruction::LoadInteger(LoadISpan { + name: SymbolKey::from("0select1"), + pos: lc(2, 12), + index: 0, + }), Instruction::PushInteger(9, lc(2, 12)), Instruction::LessEqualIntegers(lc(2, 12)), ], @@ -2774,7 +2916,11 @@ mod tests { do_compile_case_guard_test( "IS > 9", vec![ - Instruction::LoadInteger(SymbolKey::from("0select1"), lc(2, 11)), + Instruction::LoadInteger(LoadISpan { + name: SymbolKey::from("0select1"), + pos: lc(2, 11), + index: 0, + }), Instruction::PushInteger(9, lc(2, 11)), Instruction::GreaterIntegers(lc(2, 11)), ], @@ -2783,7 +2929,11 @@ mod tests { do_compile_case_guard_test( "IS >= 9", vec![ - Instruction::LoadInteger(SymbolKey::from("0select1"), lc(2, 12)), + Instruction::LoadInteger(LoadISpan { + name: SymbolKey::from("0select1"), + pos: lc(2, 12), + index: 0, + }), Instruction::PushInteger(9, lc(2, 12)), Instruction::GreaterEqualIntegers(lc(2, 12)), ], @@ -2795,10 +2945,18 @@ mod tests { do_compile_case_guard_test( "1 TO 2", vec![ - Instruction::LoadInteger(SymbolKey::from("0select1"), lc(2, 6)), + Instruction::LoadInteger(LoadISpan { + name: SymbolKey::from("0select1"), + pos: lc(2, 6), + index: 0, + }), Instruction::PushInteger(1, lc(2, 6)), Instruction::GreaterEqualIntegers(lc(2, 6)), - Instruction::LoadInteger(SymbolKey::from("0select1"), lc(2, 6)), + Instruction::LoadInteger(LoadISpan { + name: SymbolKey::from("0select1"), + pos: lc(2, 6), + index: 0, + }), Instruction::PushInteger(2, lc(2, 11)), Instruction::LessEqualIntegers(lc(2, 11)), Instruction::LogicalAnd(lc(2, 6)), @@ -2811,10 +2969,18 @@ mod tests { do_compile_case_guard_test( "IS <> 9, 8", vec![ - Instruction::LoadInteger(SymbolKey::from("0select1"), lc(2, 12)), + Instruction::LoadInteger(LoadISpan { + name: SymbolKey::from("0select1"), + pos: lc(2, 12), + index: 0, + }), Instruction::PushInteger(9, lc(2, 12)), Instruction::NotEqualIntegers(lc(2, 12)), - Instruction::LoadInteger(SymbolKey::from("0select1"), lc(2, 15)), + Instruction::LoadInteger(LoadISpan { + name: SymbolKey::from("0select1"), + pos: lc(2, 15), + index: 0, + }), Instruction::PushInteger(8, lc(2, 15)), Instruction::EqualIntegers(lc(2, 15)), Instruction::LogicalOr(lc(2, 12)), @@ -2830,12 +2996,13 @@ mod tests { .expect_instr(0, Instruction::PushInteger(5, lc(1, 13))) .expect_instr(1, Instruction::PushInteger(3, lc(1, 17))) .expect_instr(2, Instruction::AddIntegers(lc(1, 15))) - .expect_instr(3, Instruction::Assign(SymbolKey::from("0select1"))) + .expect_instr(3, Instruction::Assign(SymbolKey::from("0select1"), 0)) .expect_instr( 4, Instruction::Unset(UnsetISpan { name: SymbolKey::from("0select1"), pos: lc(1, 20), + index: 0, }), ) .check(); @@ -2848,8 +3015,15 @@ mod tests { .parse("SELECT CASE 5\nCASE 7\nFOO\nEND SELECT") .compile() .expect_instr(0, Instruction::PushInteger(5, lc(1, 13))) - .expect_instr(1, Instruction::Assign(SymbolKey::from("0select1"))) - .expect_instr(2, Instruction::LoadInteger(SymbolKey::from("0select1"), lc(2, 6))) + .expect_instr(1, Instruction::Assign(SymbolKey::from("0select1"), 0)) + .expect_instr( + 2, + Instruction::LoadInteger(LoadISpan { + name: SymbolKey::from("0select1"), + pos: lc(2, 6), + index: 0, + }), + ) .expect_instr(3, Instruction::PushInteger(7, lc(2, 6))) .expect_instr(4, Instruction::EqualIntegers(lc(2, 6))) .expect_instr(5, Instruction::JumpIfNotTrue(7)) @@ -2864,7 +3038,11 @@ mod tests { ) .expect_instr( 7, - Instruction::Unset(UnsetISpan { name: SymbolKey::from("0select1"), pos: lc(4, 1) }), + Instruction::Unset(UnsetISpan { + name: SymbolKey::from("0select1"), + pos: lc(4, 1), + index: 0, + }), ) .check(); } @@ -2875,13 +3053,21 @@ mod tests { .define("i", SymbolPrototype::Variable(ExprType::Integer)) .parse("SELECT CASE i: END SELECT") .compile() - .expect_instr(0, Instruction::LoadInteger(SymbolKey::from("i"), lc(1, 13))) - .expect_instr(1, Instruction::Assign(SymbolKey::from("0select1"))) + .expect_instr( + 0, + Instruction::LoadInteger(LoadISpan { + name: SymbolKey::from("i"), + pos: lc(1, 13), + index: 0, + }), + ) + .expect_instr(1, Instruction::Assign(SymbolKey::from("0select1"), 1)) .expect_instr( 2, Instruction::Unset(UnsetISpan { name: SymbolKey::from("0select1"), pos: lc(1, 16), + index: 1, }), ) .check(); @@ -2894,7 +3080,7 @@ mod tests { .parse("SELECT CASE 5\nCASE ELSE\nFOO\nEND SELECT") .compile() .expect_instr(0, Instruction::PushInteger(5, lc(1, 13))) - .expect_instr(1, Instruction::Assign(SymbolKey::from("0select1"))) + .expect_instr(1, Instruction::Assign(SymbolKey::from("0select1"), 0)) .expect_instr( 2, Instruction::BuiltinCall(BuiltinCallISpan { @@ -2906,7 +3092,11 @@ mod tests { ) .expect_instr( 3, - Instruction::Unset(UnsetISpan { name: SymbolKey::from("0select1"), pos: lc(4, 1) }), + Instruction::Unset(UnsetISpan { + name: SymbolKey::from("0select1"), + pos: lc(4, 1), + index: 0, + }), ) .check(); } @@ -2919,8 +3109,15 @@ mod tests { .parse("SELECT CASE 5\nCASE 7\nFOO\nCASE IS <> 8\nBAR\nEND SELECT") .compile() .expect_instr(0, Instruction::PushInteger(5, lc(1, 13))) - .expect_instr(1, Instruction::Assign(SymbolKey::from("0select1"))) - .expect_instr(2, Instruction::LoadInteger(SymbolKey::from("0select1"), lc(2, 6))) + .expect_instr(1, Instruction::Assign(SymbolKey::from("0select1"), 0)) + .expect_instr( + 2, + Instruction::LoadInteger(LoadISpan { + name: SymbolKey::from("0select1"), + pos: lc(2, 6), + index: 0, + }), + ) .expect_instr(3, Instruction::PushInteger(7, lc(2, 6))) .expect_instr(4, Instruction::EqualIntegers(lc(2, 6))) .expect_instr(5, Instruction::JumpIfNotTrue(8)) @@ -2934,7 +3131,14 @@ mod tests { }), ) .expect_instr(7, Instruction::Jump(JumpISpan { addr: 13 })) - .expect_instr(8, Instruction::LoadInteger(SymbolKey::from("0select1"), lc(4, 12))) + .expect_instr( + 8, + Instruction::LoadInteger(LoadISpan { + name: SymbolKey::from("0select1"), + pos: lc(4, 12), + index: 0, + }), + ) .expect_instr(9, Instruction::PushInteger(8, lc(4, 12))) .expect_instr(10, Instruction::NotEqualIntegers(lc(4, 12))) .expect_instr(11, Instruction::JumpIfNotTrue(13)) @@ -2949,7 +3153,11 @@ mod tests { ) .expect_instr( 13, - Instruction::Unset(UnsetISpan { name: SymbolKey::from("0select1"), pos: lc(6, 1) }), + Instruction::Unset(UnsetISpan { + name: SymbolKey::from("0select1"), + pos: lc(6, 1), + index: 0, + }), ) .check(); } @@ -2962,8 +3170,15 @@ mod tests { .parse("SELECT CASE 5\nCASE 7\nFOO\nCASE ELSE\nBAR\nEND SELECT") .compile() .expect_instr(0, Instruction::PushInteger(5, lc(1, 13))) - .expect_instr(1, Instruction::Assign(SymbolKey::from("0select1"))) - .expect_instr(2, Instruction::LoadInteger(SymbolKey::from("0select1"), lc(2, 6))) + .expect_instr(1, Instruction::Assign(SymbolKey::from("0select1"), 0)) + .expect_instr( + 2, + Instruction::LoadInteger(LoadISpan { + name: SymbolKey::from("0select1"), + pos: lc(2, 6), + index: 0, + }), + ) .expect_instr(3, Instruction::PushInteger(7, lc(2, 6))) .expect_instr(4, Instruction::EqualIntegers(lc(2, 6))) .expect_instr(5, Instruction::JumpIfNotTrue(8)) @@ -2988,7 +3203,11 @@ mod tests { ) .expect_instr( 9, - Instruction::Unset(UnsetISpan { name: SymbolKey::from("0select1"), pos: lc(6, 1) }), + Instruction::Unset(UnsetISpan { + name: SymbolKey::from("0select1"), + pos: lc(6, 1), + index: 0, + }), ) .check(); } @@ -2999,21 +3218,23 @@ mod tests { .parse("SELECT CASE 0: END SELECT\nSELECT CASE 0: END SELECT") .compile() .expect_instr(0, Instruction::PushInteger(0, lc(1, 13))) - .expect_instr(1, Instruction::Assign(SymbolKey::from("0select1"))) + .expect_instr(1, Instruction::Assign(SymbolKey::from("0select1"), 0)) .expect_instr( 2, Instruction::Unset(UnsetISpan { name: SymbolKey::from("0select1"), pos: lc(1, 16), + index: 0, }), ) .expect_instr(3, Instruction::PushInteger(0, lc(2, 13))) - .expect_instr(4, Instruction::Assign(SymbolKey::from("0select2"))) + .expect_instr(4, Instruction::Assign(SymbolKey::from("0select2"), 1)) .expect_instr( 5, Instruction::Unset(UnsetISpan { name: SymbolKey::from("0select2"), pos: lc(2, 16), + index: 1, }), ) .check(); diff --git a/core/src/compiler/symtable.rs b/core/src/compiler/symtable.rs new file mode 100644 index 00000000..2e90827d --- /dev/null +++ b/core/src/compiler/symtable.rs @@ -0,0 +1,392 @@ +// EndBASIC +// Copyright 2026 Julio Merino +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy +// of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +use crate::ast::ExprType; +use crate::syms::{CallableMetadata, Symbol, SymbolKey, Symbols}; +#[cfg(test)] +use std::collections::hash_map::Keys; +use std::collections::HashMap; +#[cfg(test)] +use std::collections::HashSet; +use std::hash::Hash; + +/// Interface to return the discriminant of an enum for index calculation purposes. +/// +/// This is not `std::mem::discriminant` because 1. we don't want to have to construct a default +/// instance just to get the discriminant, and because 2. we may want to collapse multiple types +/// into the same bucket. +trait Bucketizer { + /// The type being bucketized. + type V; + + /// Bucketizes a value and assigns it a "bucket" number. + fn bucketize(&self, value: &Self::V) -> u8; +} + +/// Information about a symbol in the symbols table. +#[derive(Clone)] +pub(super) enum SymbolPrototype { + /// Information about an array. The integer indicates the number of dimensions in the array. + Array(ExprType, usize), + + /// Information about a callable that's a builtin and requires an upcall to execute. + /// The integer indicates the runtime upcall index of the callable. + BuiltinCallable(CallableMetadata), + + /// Information about a callable. + Callable(CallableMetadata), + + /// Information about a variable. + Variable(ExprType), +} + +/// A bucketizer for `SymbolPrototype`. +#[derive(Default)] +struct SymbolPrototypeBucketizer {} + +impl SymbolPrototypeBucketizer { + const BUCKET_BUILTINS: u8 = 0; + const BUCKET_STACK: u8 = 1; + const BUCKET_OTHER: u8 = 255; +} + +impl Bucketizer for SymbolPrototypeBucketizer { + type V = SymbolPrototype; + + fn bucketize(&self, value: &SymbolPrototype) -> u8 { + match value { + SymbolPrototype::BuiltinCallable(..) => Self::BUCKET_BUILTINS, + SymbolPrototype::Array(..) | SymbolPrototype::Variable(..) => Self::BUCKET_STACK, + _ => Self::BUCKET_OTHER, + } + } +} + +/// A wrapper over `HashMap` that assigns indexes to newly-inserted entries based on a `Bucketizer` +/// and then allows retrieving such indexes per key and retrieving the list of keys in insertion +/// order for a given bucket. +struct IndexedHashMap { + map: HashMap, + bucketizer: B, + counters: HashMap, +} + +impl> IndexedHashMap { + /// Constructs a new `IndexedHashMap` backed by `bucketizer` to assign indexes. + fn new(bucketizer: B) -> Self { + Self { map: HashMap::default(), bucketizer, counters: HashMap::default() } + } + + /// Returns the counter of the bucket where `value` belongs. + fn counts_of(&self, value: &V) -> usize { + let counter_id = self.bucketizer.bucketize(value); + self.counters.get(&counter_id).copied().unwrap_or(0) + } + + /// Increments the counter of the bucket where `value` belongs. + fn increment_counts_of(&mut self, value: &V) -> usize { + let counter_id = self.bucketizer.bucketize(value); + let next_index = *self.counters.entry(counter_id).and_modify(|v| *v += 1).or_insert(1); + next_index - 1 + } +} + +impl> IndexedHashMap { + /// Same as `HashMap::contains_key`. + fn contains_key(&self, key: &K) -> bool { + self.map.contains_key(key) + } + + /// Same as `HashMap::get`. + #[cfg(test)] + fn get(&self, key: &K) -> Option<&V> { + self.map.get(key).map(|v| &v.0) + } + + /// Same as `HashMap::get` but also returns the index assigned to the key. + fn get_with_index(&self, key: &K) -> Option<(&V, usize)> { + self.map.get(key).map(|v| (&v.0, v.1)) + } + + /// Same as `HashMap::insert` but does not return the previous value because this assumes that + /// no previous value can exist. Instead, this returns the assigned index. + fn insert(&mut self, key: K, value: V) -> usize { + let index = self.increment_counts_of(&value); + let previous = self.map.insert(key, (value, index)); + // We could support updating existing keys, but that would make things more difficult for no + // reason because we would need to check if the replacement value matches the bucket of the + // previous one and deal with that accordingly. + assert!(previous.is_none(), "Updating existing keys is not supported"); + index + } + + /// Same as `HashMap::keys`. + #[cfg(test)] + fn keys(&self) -> Keys<'_, K, (V, usize)> { + self.map.keys() + } + + /// Same as `HashMap::remove`. + fn remove(&mut self, key: &K) -> Option { + self.map.remove(key).map(|v| v.0) + } +} + +impl> IndexedHashMap { + /// Returns the list of keys, in insertion order, for `bucket`. + fn get_ordered_keys(&self, bucket: u8) -> Vec { + let mut builtins = self + .map + .iter() + .filter_map(|(key, (proto, index))| { + if self.bucketizer.bucketize(proto) == bucket { + Some((index, key)) + } else { + None + } + }) + .collect::>(); + builtins.sort_by_key(|(index, _key)| *index); + builtins.into_iter().map(|(_index, key)| key.clone()).collect() + } +} + +type SymbolsMap = IndexedHashMap; + +/// The symbols table used during compilation. +/// +/// Symbols are represented as a two-layer map: the globals map contains all symbols that are +/// visible by all scopes, and the scope contains all symbols that are only visible within a +/// given scope. +/// +/// The collection of symbols that is visible at any given point in time is thus the union of +/// the global symbols and the symbols in the last scope. +/// +/// There is always at least one scope in the scopes stack: at the program level outside of +/// functions, variables are not global by default and thus they are kept in their own scope. +/// But because we do not support nested function definitions, the scopes "stack" should +/// always have size of one or two. +pub(super) struct SymbolsTable { + /// Map of global symbol names to their definitions. + globals: SymbolsMap, + + /// Map of local symbol names to their definitions. + scopes: Vec, +} + +impl Default for SymbolsTable { + fn default() -> Self { + Self { + globals: IndexedHashMap::new(SymbolPrototypeBucketizer::default()), + scopes: vec![IndexedHashMap::new(SymbolPrototypeBucketizer::default())], + } + } +} + +impl From for SymbolsTable { + fn from(globals: SymbolsMap) -> Self { + Self { globals, scopes: vec![IndexedHashMap::new(SymbolPrototypeBucketizer::default())] } + } +} + +impl From<&Symbols> for SymbolsTable { + fn from(syms: &Symbols) -> Self { + let globals = { + let mut globals = IndexedHashMap::new(SymbolPrototypeBucketizer::default()); + + let callables = syms.callables(); + let mut names = callables.keys().copied().collect::>(); + // This is only necessary for testing really... but may also remove some confusion + // when inspecting the bytecode because it helps keep upcall indexes stable across + // different compilations. + names.sort(); + + for name in names { + let callable = callables.get(&name).unwrap(); + let proto = SymbolPrototype::BuiltinCallable(callable.metadata().clone()); + globals.insert(name.clone(), proto); + } + + globals + }; + + let mut scope = IndexedHashMap::new(SymbolPrototypeBucketizer::default()); + for (name, symbol) in syms.locals() { + let proto = match symbol { + Symbol::Array(array) => { + SymbolPrototype::Array(array.subtype(), array.dimensions().len()) + } + Symbol::Callable(_) => { + unreachable!("Callables must only be global"); + } + Symbol::Variable(var) => SymbolPrototype::Variable(var.as_exprtype()), + }; + scope.insert(name.clone(), proto); + } + + Self { globals, scopes: vec![scope] } + } +} + +impl SymbolsTable { + /// Enters a new scope. + pub(super) fn enter_scope(&mut self) { + self.scopes.push(IndexedHashMap::new(SymbolPrototypeBucketizer::default())); + } + + /// Leaves the current scope. + pub(super) fn leave_scope(&mut self) { + let last = self.scopes.pop(); + assert!(last.is_some(), "Must have at least one scope to pop"); + assert!(!self.scopes.is_empty(), "Cannot pop the global scope"); + } + + /// Returns true if the symbols table contains `key`. + pub(super) fn contains_key(&mut self, key: &SymbolKey) -> bool { + self.scopes.last().unwrap().contains_key(key) || self.globals.contains_key(key) + } + + /// Returns the information for the symbol `key` if it exists, otherwise `None`. + #[cfg(test)] + pub(super) fn get(&self, key: &SymbolKey) -> Option<&SymbolPrototype> { + let proto = self.scopes.last().unwrap().get(key); + if proto.is_some() { + return proto; + } + + self.globals.get(key) + } + + /// Returns the information for the symbol `key` if it exists, otherwise `None`. + pub(super) fn get_with_index(&self, key: &SymbolKey) -> Option<(&SymbolPrototype, usize)> { + let proto = self.scopes.last().unwrap().get_with_index(key); + if proto.is_some() { + return proto; + } + + self.globals.get_with_index(key) + } + + /// Inserts the new information `proto` about symbol `key` into the symbols table. + /// The symbol must not yet exist. + pub(super) fn insert(&mut self, key: SymbolKey, proto: SymbolPrototype) -> usize { + debug_assert!(!self.globals.contains_key(&key), "Cannot redefine a symbol"); + self.scopes.last_mut().unwrap().insert(key, proto) + } + + /// Inserts the builtin callable described by `md` and assigns an upcall index. + /// The symbol must not yet exist. + #[cfg(test)] + pub(super) fn insert_builtin_callable(&mut self, key: SymbolKey, md: CallableMetadata) { + debug_assert!(!self.globals.contains_key(&key), "Cannot redefine a symbol"); + let proto = SymbolPrototype::BuiltinCallable(md); + self.globals.insert(key, proto); + } + + /// Inserts the new information `proto` about symbol `key` into the symbols table. + /// The symbol must not yet exist. + pub(super) fn insert_global(&mut self, key: SymbolKey, proto: SymbolPrototype) -> usize { + self.globals.insert(key, proto) + } + + /// Returns the index of the last element of type `proto` for the current scope. + pub(super) fn last_index_of(&self, proto: &SymbolPrototype) -> usize { + self.scopes.last().unwrap().counts_of(proto) + } + + /// Removes information about the symbol `key`. + pub(super) fn remove(&mut self, key: SymbolKey) { + let previous = self.scopes.last_mut().unwrap().remove(&key); + debug_assert!(previous.is_some(), "Cannot unset a non-existing symbol"); + } + + /// Returns a view of the keys in the symbols table. + #[cfg(test)] + pub(super) fn keys(&self) -> HashSet<&SymbolKey> { + let mut keys = HashSet::default(); + keys.extend(self.globals.keys()); + keys.extend(self.scopes.last().unwrap().keys()); + keys + } + + /// Calculates the list of upcalls in this symbols table in the order in which they were + /// assigned indexes. + pub(super) fn upcalls(&self) -> Vec { + self.globals.get_ordered_keys(SymbolPrototypeBucketizer::BUCKET_BUILTINS) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// A bucketizer for integers that classifies them as even or odd. + struct I32Bucketizer {} + + impl I32Bucketizer { + const BUCKET_ODD: u8 = 10; + const BUCKET_EVEN: u8 = 20; + } + + impl Bucketizer for I32Bucketizer { + type V = i32; + + fn bucketize(&self, value: &Self::V) -> u8 { + if value % 2 == 0 { + Self::BUCKET_EVEN + } else { + Self::BUCKET_ODD + } + } + } + + #[test] + fn test_indexed_hash_map_assigns_indexes_by_bucket() { + let mut map = IndexedHashMap::new(I32Bucketizer {}); + map.insert("a", 1); + map.insert("b", 2); + map.insert("c", 2); + map.insert("d", 3); + map.insert("e", 3); + map.insert("f", 1); + map.insert("g", 4); + + assert_eq!((&1, 0), map.get_with_index(&"a").unwrap()); + assert_eq!((&2, 0), map.get_with_index(&"b").unwrap()); + assert_eq!((&2, 1), map.get_with_index(&"c").unwrap()); + assert_eq!((&3, 1), map.get_with_index(&"d").unwrap()); + assert_eq!((&3, 2), map.get_with_index(&"e").unwrap()); + assert_eq!((&1, 3), map.get_with_index(&"f").unwrap()); + assert_eq!((&4, 2), map.get_with_index(&"g").unwrap()); + } + + #[test] + fn test_indexed_hash_map_get_ordered_keys() { + let mut map = IndexedHashMap::new(I32Bucketizer {}); + map.insert("a", 1); + map.insert("h", 2); + map.insert("d", 2); + map.insert("i", 3); + map.insert("e", 3); + map.insert("z", 1); + map.insert("o", 4); + + assert_eq!(["h", "d", "o"], map.get_ordered_keys(I32Bucketizer::BUCKET_EVEN).as_slice()); + assert_eq!( + ["a", "i", "e", "z"], + map.get_ordered_keys(I32Bucketizer::BUCKET_ODD).as_slice() + ); + } +} diff --git a/core/src/exec.rs b/core/src/exec.rs index cd4d6067..36b1cea0 100644 --- a/core/src/exec.rs +++ b/core/src/exec.rs @@ -687,19 +687,36 @@ impl Machine { } /// Handles a builtin call. - async fn builtin_call( + async fn upcall( &mut self, context: &mut Context, callable: Rc, + return_type: Option, bref_pos: LineCol, nargs: usize, ) -> Result<()> { let metadata = callable.metadata(); - debug_assert!(!metadata.is_function()); - let scope = Scope::new(&mut context.value_stack, nargs, bref_pos); - - callable.exec(scope, self).await + callable.exec(scope, self).await?; + if cfg!(debug_assertions) { + if let Some(return_type) = return_type { + debug_assert!( + metadata.is_function(), + "Return types are only allowed for functions" + ); + match context.value_stack.top() { + Some((value, _pos)) => { + debug_assert_eq!( + return_type, + value.as_exprtype(), + "Value returned by function is incompatible with its type definition", + ) + } + None => unreachable!("Functions must return one value"), + } + } + } + Ok(()) } /// Handles an array definition. The array must not yet exist, and the name may not overlap @@ -897,35 +914,6 @@ impl Machine { Ok(subscripts) } - /// Evaluates a function call specified by `fref` and arguments `args` on the function `f`. - async fn do_function_call( - &mut self, - context: &mut Context, - return_type: ExprType, - fref_pos: LineCol, - nargs: usize, - f: Rc, - ) -> Result<()> { - let metadata = f.metadata(); - debug_assert_eq!(return_type, metadata.return_type().unwrap()); - - let scope = Scope::new(&mut context.value_stack, nargs, fref_pos); - f.exec(scope, self).await?; - if cfg!(debug_assertions) { - match context.value_stack.top() { - Some((value, _pos)) => { - debug_assert_eq!( - return_type, - value.as_exprtype(), - "Value returned by function is incompatible with its type definition", - ) - } - None => unreachable!("Functions must return one value"), - } - } - Ok(()) - } - /// Handles an array reference. fn array_ref( &mut self, @@ -949,55 +937,6 @@ impl Machine { } } - /// Handles a function call. - async fn function_call( - &mut self, - context: &mut Context, - callable: Rc, - name: &SymbolKey, - return_type: ExprType, - fref_pos: LineCol, - nargs: usize, - ) -> Result<()> { - if !callable.metadata().is_function() { - return Err(Error::EvalError( - fref_pos, - format!("{} is not an array nor a function", callable.metadata().name()), - )); - } - if callable.metadata().is_argless() { - self.argless_function_call(context, name, return_type, fref_pos, callable).await - } else { - self.do_function_call(context, return_type, fref_pos, nargs, callable).await - } - } - - /// Evaluates a call to an argless function. - async fn argless_function_call( - &mut self, - context: &mut Context, - fname: &SymbolKey, - ftype: ExprType, - fpos: LineCol, - f: Rc, - ) -> Result<()> { - let scope = Scope::new(&mut context.value_stack, 0, fpos); - f.exec(scope, self).await?; - if cfg!(debug_assertions) { - match context.value_stack.top() { - Some((value, _pos)) => { - let fref_checker = VarRef::new(fname.to_string(), Some(ftype)); - debug_assert!( - fref_checker.accepts(value.as_exprtype()), - "Value returned by function is incompatible with its type definition", - ) - } - None => unreachable!("Functions must return one value"), - } - } - Ok(()) - } - /// Loads the value of a symbol. fn load(&self, key: &SymbolKey, pos: LineCol) -> Result<&Value> { match self.symbols.load(key) { @@ -1246,19 +1185,19 @@ impl Machine { context.pc += 1; } - Instruction::Assign(key) => { + Instruction::Assign(key, _index) => { let (value, _pos) = context.value_stack.pop().unwrap(); self.symbols.assign(key, value); context.pc += 1; } - Instruction::ArrayAssignment(name, vref_pos, nargs) => { - self.assign_array(context, name, *vref_pos, *nargs)?; + Instruction::ArrayAssignment(span) => { + self.assign_array(context, &span.name, span.name_pos, span.nargs)?; context.pc += 1; } - Instruction::ArrayLoad(name, pos, nargs) => { - self.array_ref(context, name, *pos, *nargs)?; + Instruction::ArrayLoad(span) => { + self.array_ref(context, &span.name, span.name_pos, span.nargs)?; context.pc += 1; } @@ -1376,44 +1315,44 @@ impl Machine { context.pc += 1; } - Instruction::LoadBoolean(key, pos) => { - let b = match self.load(key, *pos)? { + Instruction::LoadBoolean(span) => { + let b = match self.load(&span.name, span.pos)? { Value::Boolean(b) => b, _ => unreachable!("Types are validated at compilation time"), }; - context.value_stack.push_boolean(*b, *pos); + context.value_stack.push_boolean(*b, span.pos); context.pc += 1; } - Instruction::LoadDouble(key, pos) => { - let d = match self.load(key, *pos)? { + Instruction::LoadDouble(span) => { + let d = match self.load(&span.name, span.pos)? { Value::Double(d) => d, _ => unreachable!("Types are validated at compilation time"), }; - context.value_stack.push_double(*d, *pos); + context.value_stack.push_double(*d, span.pos); context.pc += 1; } - Instruction::LoadInteger(key, pos) => { - let i = match self.load(key, *pos)? { + Instruction::LoadInteger(span) => { + let i = match self.load(&span.name, span.pos)? { Value::Integer(i) => i, _ => unreachable!("Types are validated at compilation time"), }; - context.value_stack.push_integer(*i, *pos); + context.value_stack.push_integer(*i, span.pos); context.pc += 1; } - Instruction::LoadString(key, pos) => { - let s = match self.load(key, *pos)? { + Instruction::LoadString(span) => { + let s = match self.load(&span.name, span.pos)? { Value::Text(s) => s, _ => unreachable!("Types are validated at compilation time"), }; - context.value_stack.push_string(s.clone(), *pos); + context.value_stack.push_string(s.clone(), span.pos); context.pc += 1; } - Instruction::LoadRef(key, etype, pos) => { - context.value_stack.push_varref(key.clone(), *etype, *pos); + Instruction::LoadRef(span, etype) => { + context.value_stack.push_varref(span.name.clone(), *etype, span.pos); context.pc += 1; } @@ -1526,22 +1465,9 @@ impl Machine { Ok(InternalStopReason::Upcall(data)) => { let upcall = upcalls[data.index].clone(); - let result; - if let Some(return_type) = data.return_type { - result = self - .function_call( - &mut context, - upcall, - &data.name, - return_type, - data.pos, - data.nargs, - ) - .await; - } else { - result = - self.builtin_call(&mut context, upcall, data.pos, data.nargs).await; - } + let result = self + .upcall(&mut context, upcall, data.return_type, data.pos, data.nargs) + .await; match result { Ok(()) => context.pc += 1, Err(e) => self.handle_error(instrs, &mut context, e)?, @@ -3475,7 +3401,7 @@ mod tests { END FUNCTION OUT foo(3, 4) "#; - do_error_test(code, &[], &[], "5:17: FOO expected n%"); + do_error_test(code, &[], &[], "5:17: FOO expected (n%)"); } #[test] diff --git a/core/src/syms.rs b/core/src/syms.rs index b3c436ab..96a46e63 100644 --- a/core/src/syms.rs +++ b/core/src/syms.rs @@ -630,23 +630,30 @@ impl CallableMetadata { } /// Gets the callable's syntax specification. - pub fn syntax(&self) -> String { - fn format_one(cs: &CallableSyntax) -> String { - let mut syntax = cs.describe(); - if syntax.is_empty() { - syntax.push_str("no arguments"); + /// + /// If the callable takes no arguments at all, this returns None to force the caller into + /// deciding how to format it. + pub fn syntax(&self) -> Option { + fn format_one(cs: &CallableSyntax, left_sep: &str, right_sep: &str) -> String { + let description = cs.describe(); + if description.is_empty() { + "".to_owned() + } else { + format!("{}{}{}", left_sep, description, right_sep) } - syntax } + let (left_sep, right_sep) = if self.is_function() { ("(", ")") } else { ("", "") }; match self.syntaxes.as_slice() { [] => panic!("Callables without syntaxes are not allowed at construction time"), - [one] => format_one(one), - many => many - .iter() - .map(|syn| format!("<{}>", syn.describe())) - .collect::>() - .join(" | "), + [one] if one.is_empty() => None, + [one] => Some(format_one(one, left_sep, right_sep)), + many => Some( + many.iter() + .map(|syn| format_one(syn, left_sep, right_sep)) + .collect::>() + .join(" | "), + ), } } @@ -667,9 +674,9 @@ impl CallableMetadata { self.description.lines() } - /// Returns true if this is a callable that takes no arguments. + /// Returns true if this is a callable that may take no arguments. pub fn is_argless(&self) -> bool { - self.syntaxes.is_empty() || (self.syntaxes.len() == 1 && self.syntaxes[0].is_empty()) + self.syntaxes.iter().filter(|syn| syn.is_empty()).count() > 0 } /// Returns true if this callable is a function (not a command). diff --git a/std/src/arrays.rs b/std/src/arrays.rs index 60df1641..e627fc67 100644 --- a/std/src/arrays.rs +++ b/std/src/arrays.rs @@ -231,7 +231,7 @@ mod tests { Tester::default() .run(format!("DIM x(2): result = {}()", func)) .expect_compilation_err(format!( - "1:20: {} expected | ", + "1:20: {} expected (array) | (array, dimension%)", func )) .check(); @@ -239,7 +239,7 @@ mod tests { Tester::default() .run(format!("DIM x(2): result = {}(x, 1, 2)", func)) .expect_compilation_err(format!( - "1:20: {} expected | ", + "1:20: {} expected (array) | (array, dimension%)", func )) .check(); diff --git a/std/src/console/cmds.rs b/std/src/console/cmds.rs index 48fb1d55..c8da6eba 100644 --- a/std/src/console/cmds.rs +++ b/std/src/console/cmds.rs @@ -716,10 +716,13 @@ mod tests { #[test] fn test_color_errors() { check_stmt_compilation_err( - "1:1: COLOR expected <> | | <[fg%], [bg%]>", + "1:1: COLOR expected | fg% | [fg%], [bg%]", "COLOR 1, 2, 3", ); - check_stmt_compilation_err("1:1: COLOR expected <> | | <[fg%], [bg%]>", "COLOR 1; 2"); + check_stmt_compilation_err( + "1:1: COLOR expected | fg% | [fg%], [bg%]", + "COLOR 1; 2", + ); check_stmt_err("1:7: Color out of range", "COLOR 1000, 0"); check_stmt_err("1:10: Color out of range", "COLOR 0, 1000"); @@ -844,18 +847,12 @@ mod tests { #[test] fn test_input_errors() { - check_stmt_compilation_err("1:1: INPUT expected | <[prompt$] <,|;> vref>", "INPUT"); - check_stmt_compilation_err( - "1:1: INPUT expected | <[prompt$] <,|;> vref>", - "INPUT ; ,", - ); - check_stmt_compilation_err( - "1:1: INPUT expected | <[prompt$] <,|;> vref>", - "INPUT ;", - ); + check_stmt_compilation_err("1:1: INPUT expected vref | [prompt$] <,|;> vref", "INPUT"); + check_stmt_compilation_err("1:1: INPUT expected vref | [prompt$] <,|;> vref", "INPUT ; ,"); + check_stmt_compilation_err("1:1: INPUT expected vref | [prompt$] <,|;> vref", "INPUT ;"); check_stmt_compilation_err("1:7: expected STRING but found INTEGER", "INPUT 3 ; a"); check_stmt_compilation_err( - "1:1: INPUT expected | <[prompt$] <,|;> vref>", + "1:1: INPUT expected vref | [prompt$] <,|;> vref", "INPUT \"foo\" AS bar", ); check_stmt_err("1:7: Undefined symbol A", "INPUT a + 1 ; b"); diff --git a/std/src/gfx/mod.rs b/std/src/gfx/mod.rs index abf7ac00..fc11f5aa 100644 --- a/std/src/gfx/mod.rs +++ b/std/src/gfx/mod.rs @@ -911,7 +911,10 @@ mod tests { #[test] fn test_gfx_sync_errors() { - check_stmt_compilation_err("1:1: GFX_SYNC expected <> | ", "GFX_SYNC 2, 3"); + check_stmt_compilation_err( + "1:1: GFX_SYNC expected | enabled?", + "GFX_SYNC 2, 3", + ); check_stmt_compilation_err("1:10: expected BOOLEAN but found INTEGER", "GFX_SYNC 2"); } diff --git a/std/src/gpio/mod.rs b/std/src/gpio/mod.rs index 242efb23..b829f35e 100644 --- a/std/src/gpio/mod.rs +++ b/std/src/gpio/mod.rs @@ -515,8 +515,14 @@ mod tests { #[test] fn test_gpio_clear_errors() { - check_stmt_compilation_err("1:1: GPIO_CLEAR expected <> | ", r#"GPIO_CLEAR 1,"#); - check_stmt_compilation_err("1:1: GPIO_CLEAR expected <> | ", r#"GPIO_CLEAR 1, 2"#); + check_stmt_compilation_err( + "1:1: GPIO_CLEAR expected | pin%", + r#"GPIO_CLEAR 1,"#, + ); + check_stmt_compilation_err( + "1:1: GPIO_CLEAR expected | pin%", + r#"GPIO_CLEAR 1, 2"#, + ); check_pin_validation("1:12: ", "1:12: ", r#"GPIO_CLEAR _PIN_"#); } @@ -535,8 +541,8 @@ mod tests { #[test] fn test_gpio_read_errors() { - check_expr_compilation_error("1:10: GPIO_READ expected pin%", r#"GPIO_READ()"#); - check_expr_compilation_error("1:10: GPIO_READ expected pin%", r#"GPIO_READ(1, 2)"#); + check_expr_compilation_error("1:10: GPIO_READ expected (pin%)", r#"GPIO_READ()"#); + check_expr_compilation_error("1:10: GPIO_READ expected (pin%)", r#"GPIO_READ(1, 2)"#); check_pin_validation("1:15: ", "1:15: ", r#"v = GPIO_READ(_PIN_)"#); } diff --git a/std/src/help.rs b/std/src/help.rs index 585eb7a1..23487263 100644 --- a/std/src/help.rs +++ b/std/src/help.rs @@ -87,43 +87,26 @@ impl Topic for CallableTopic { } async fn describe(&self, pager: &mut Pager<'_>) -> io::Result<()> { + let syntax = match self.metadata.syntax() { + None => "".to_owned(), + Some(s) => format!(" {}", s), + }; + pager.print("").await?; let previous = pager.color(); pager.set_color(Some(TITLE_COLOR), previous.1)?; match self.metadata.return_type() { None => { - if self.metadata.is_argless() { - refill_and_page(pager, [self.metadata.name()], " ").await?; - } else { - refill_and_page( - pager, - [&format!("{} {}", self.metadata.name(), self.metadata.syntax())], - " ", - ) + refill_and_page(pager, [&format!("{} {}", self.metadata.name(), syntax)], " ") .await?; - } } Some(return_type) => { - if self.metadata.is_argless() { - refill_and_page( - pager, - [&format!("{}{}", self.metadata.name(), return_type.annotation(),)], - " ", - ) - .await?; - } else { - refill_and_page( - pager, - [&format!( - "{}{}({})", - self.metadata.name(), - return_type.annotation(), - self.metadata.syntax(), - )], - " ", - ) - .await?; - } + refill_and_page( + pager, + [&format!("{}{} {}", self.metadata.name(), return_type.annotation(), syntax,)], + " ", + ) + .await?; } } pager.set_color(previous.0, previous.1)?; @@ -132,6 +115,7 @@ impl Topic for CallableTopic { refill_and_page(pager, self.metadata.description(), " ").await?; } pager.print("").await?; + Ok(()) } } @@ -820,7 +804,7 @@ This is the first and only topic with just one line. .expect_prints([""]) .expect_output([ CapturedOut::SetColor(Some(TITLE_COLOR), Some(26)), - CapturedOut::Print(" EMPTY$(sample$)".to_owned()), + CapturedOut::Print(" EMPTY$ (sample$)".to_owned()), CapturedOut::SetColor(Some(30), Some(26)), ]) .expect_prints([ @@ -873,7 +857,7 @@ This is the first and only topic with just one line. fn test_help_prefix_search() { fn exp_output(name: &str, is_function: bool) -> Vec { let spec = if is_function { - format!(" {}(sample$)", name) + format!(" {} (sample$)", name) } else { format!(" {} sample$", name) }; @@ -938,7 +922,7 @@ This is the first and only topic with just one line. t.run(r#"HELP foo"#).expect_compilation_err("1:6: Undefined symbol FOO").check(); t.run(r#"HELP "foo", 3"#) - .expect_compilation_err("1:1: HELP expected <> | ") + .expect_compilation_err("1:1: HELP expected | topic$") .check(); t.run(r#"HELP 3"#).expect_compilation_err("1:6: expected STRING but found INTEGER").check(); diff --git a/std/src/numerics.rs b/std/src/numerics.rs index 53a9642b..5fffc831 100644 --- a/std/src/numerics.rs +++ b/std/src/numerics.rs @@ -769,9 +769,9 @@ mod tests { check_expr_ok_with_vars(123f64.atan(), "ATN(a)", [("a", 123i32.into())]); - check_expr_compilation_error("1:10: ATN expected n#", "ATN()"); + check_expr_compilation_error("1:10: ATN expected (n#)", "ATN()"); check_expr_compilation_error("1:14: BOOLEAN is not a number", "ATN(FALSE)"); - check_expr_compilation_error("1:10: ATN expected n#", "ATN(3, 4)"); + check_expr_compilation_error("1:10: ATN expected (n#)", "ATN(3, 4)"); } #[test] @@ -783,9 +783,9 @@ mod tests { check_expr_ok_with_vars(1, "CINT(d)", [("d", 0.9f64.into())]); - check_expr_compilation_error("1:10: CINT expected expr#", "CINT()"); + check_expr_compilation_error("1:10: CINT expected (expr#)", "CINT()"); check_expr_compilation_error("1:15: BOOLEAN is not a number", "CINT(FALSE)"); - check_expr_compilation_error("1:10: CINT expected expr#", "CINT(3.0, 4)"); + check_expr_compilation_error("1:10: CINT expected (expr#)", "CINT(3.0, 4)"); check_expr_error( "1:15: Cannot cast -1234567890123456 to integer due to overflow", @@ -800,9 +800,9 @@ mod tests { check_expr_ok_with_vars(123f64.cos(), "COS(i)", [("i", 123i32.into())]); - check_expr_compilation_error("1:10: COS expected angle#", "COS()"); + check_expr_compilation_error("1:10: COS expected (angle#)", "COS()"); check_expr_compilation_error("1:14: BOOLEAN is not a number", "COS(FALSE)"); - check_expr_compilation_error("1:10: COS expected angle#", "COS(3, 4)"); + check_expr_compilation_error("1:10: COS expected (angle#)", "COS(3, 4)"); } #[test] @@ -836,9 +836,9 @@ mod tests { check_expr_ok_with_vars(0, "INT(d)", [("d", 0.9f64.into())]); - check_expr_compilation_error("1:10: INT expected expr#", "INT()"); + check_expr_compilation_error("1:10: INT expected (expr#)", "INT()"); check_expr_compilation_error("1:14: BOOLEAN is not a number", "INT(FALSE)"); - check_expr_compilation_error("1:10: INT expected expr#", "INT(3.0, 4)"); + check_expr_compilation_error("1:10: INT expected (expr#)", "INT(3.0, 4)"); check_expr_error( "1:14: Cannot cast -1234567890123456 to integer due to overflow", @@ -870,7 +870,7 @@ mod tests { [("i", 5i32.into()), ("j", 3i32.into()), ("k", 4i32.into())], ); - check_expr_compilation_error("1:10: MAX expected expr1#[, .., exprN#]", "MAX()"); + check_expr_compilation_error("1:10: MAX expected (expr1#[, .., exprN#])", "MAX()"); check_expr_compilation_error("1:14: BOOLEAN is not a number", "MAX(FALSE)"); } @@ -898,7 +898,7 @@ mod tests { [("i", 5i32.into()), ("j", 3i32.into()), ("k", 4i32.into())], ); - check_expr_compilation_error("1:10: MIN expected expr1#[, .., exprN#]", "MIN()"); + check_expr_compilation_error("1:10: MIN expected (expr1#[, .., exprN#])", "MIN()"); check_expr_compilation_error("1:14: BOOLEAN is not a number", "MIN(FALSE)"); } @@ -929,13 +929,13 @@ mod tests { t.run("RANDOMIZE 10.2").expect_var("result", 0.8273883964464507).check(); - t.run("result = RND(1)").expect_var("result", 0.7097578208683426).check(); + t.run("result = RND").expect_var("result", 0.7097578208683426).check(); - check_expr_compilation_error("1:10: RND expected <> | ", "RND(1, 7)"); + check_expr_compilation_error("1:10: RND expected | (n%)", "RND(1, 7)"); check_expr_compilation_error("1:14: BOOLEAN is not a number", "RND(FALSE)"); check_expr_error("1:14: n% cannot be negative", "RND(-1)"); - check_stmt_compilation_err("1:1: RANDOMIZE expected <> | ", "RANDOMIZE ,"); + check_stmt_compilation_err("1:1: RANDOMIZE expected | seed%", "RANDOMIZE ,"); check_stmt_compilation_err("1:11: BOOLEAN is not a number", "RANDOMIZE TRUE"); } @@ -946,9 +946,9 @@ mod tests { check_expr_ok_with_vars(123f64.sin(), "SIN(i)", [("i", 123i32.into())]); - check_expr_compilation_error("1:10: SIN expected angle#", "SIN()"); + check_expr_compilation_error("1:10: SIN expected (angle#)", "SIN()"); check_expr_compilation_error("1:14: BOOLEAN is not a number", "SIN(FALSE)"); - check_expr_compilation_error("1:10: SIN expected angle#", "SIN(3, 4)"); + check_expr_compilation_error("1:10: SIN expected (angle#)", "SIN(3, 4)"); } #[test] @@ -960,9 +960,9 @@ mod tests { check_expr_ok_with_vars(9f64.sqrt(), "SQR(i)", [("i", 9i32.into())]); - check_expr_compilation_error("1:10: SQR expected num#", "SQR()"); + check_expr_compilation_error("1:10: SQR expected (num#)", "SQR()"); check_expr_compilation_error("1:14: BOOLEAN is not a number", "SQR(FALSE)"); - check_expr_compilation_error("1:10: SQR expected num#", "SQR(3, 4)"); + check_expr_compilation_error("1:10: SQR expected (num#)", "SQR(3, 4)"); check_expr_error("1:14: Cannot take square root of a negative number", "SQR(-3)"); check_expr_error("1:14: Cannot take square root of a negative number", "SQR(-0.1)"); } @@ -974,8 +974,8 @@ mod tests { check_expr_ok_with_vars(123f64.tan(), "TAN(i)", [("i", 123i32.into())]); - check_expr_compilation_error("1:10: TAN expected angle#", "TAN()"); + check_expr_compilation_error("1:10: TAN expected (angle#)", "TAN()"); check_expr_compilation_error("1:14: BOOLEAN is not a number", "TAN(FALSE)"); - check_expr_compilation_error("1:10: TAN expected angle#", "TAN(3, 4)"); + check_expr_compilation_error("1:10: TAN expected (angle#)", "TAN(3, 4)"); } } diff --git a/std/src/program.rs b/std/src/program.rs index 2b82a45d..5b11eb9b 100644 --- a/std/src/program.rs +++ b/std/src/program.rs @@ -996,7 +996,7 @@ mod tests { Tester::default() .run("SAVE 2, 3") - .expect_compilation_err("1:1: SAVE expected <> | ") + .expect_compilation_err("1:1: SAVE expected | filename$") .check(); } } diff --git a/std/src/storage/cmds.rs b/std/src/storage/cmds.rs index 59808c4d..3e32b70f 100644 --- a/std/src/storage/cmds.rs +++ b/std/src/storage/cmds.rs @@ -830,7 +830,7 @@ mod tests { #[test] fn test_dir_errors() { - check_stmt_compilation_err("1:1: DIR expected <> | ", "DIR 2, 3"); + check_stmt_compilation_err("1:1: DIR expected | path$", "DIR 2, 3"); check_stmt_compilation_err("1:5: expected STRING but found INTEGER", "DIR 2"); } @@ -923,9 +923,12 @@ mod tests { #[test] fn test_mount_errors() { - check_stmt_compilation_err("1:1: MOUNT expected <> | ", "MOUNT 1"); check_stmt_compilation_err( - "1:1: MOUNT expected <> | ", + "1:1: MOUNT expected | target$ AS drive_name$", + "MOUNT 1", + ); + check_stmt_compilation_err( + "1:1: MOUNT expected | target$ AS drive_name$", "MOUNT 1, 2, 3", ); diff --git a/std/src/strings.rs b/std/src/strings.rs index 573794d3..b144efbb 100644 --- a/std/src/strings.rs +++ b/std/src/strings.rs @@ -702,9 +702,9 @@ mod tests { check_expr_ok_with_vars('a' as i32, r#"ASC(s)"#, [("s", "a".into())]); - check_expr_compilation_error("1:10: ASC expected char$", r#"ASC()"#); + check_expr_compilation_error("1:10: ASC expected (char$)", r#"ASC()"#); check_expr_compilation_error("1:14: expected STRING but found INTEGER", r#"ASC(3)"#); - check_expr_compilation_error("1:10: ASC expected char$", r#"ASC("a", 1)"#); + check_expr_compilation_error("1:10: ASC expected (char$)", r#"ASC("a", 1)"#); check_expr_error("1:14: Input string \"\" must be 1-character long", r#"ASC("")"#); check_expr_error("1:14: Input string \"ab\" must be 1-character long", r#"ASC("ab")"#); } @@ -718,9 +718,9 @@ mod tests { check_expr_ok_with_vars(" ", r#"CHR(i)"#, [("i", 32.into())]); - check_expr_compilation_error("1:10: CHR expected code%", r#"CHR()"#); + check_expr_compilation_error("1:10: CHR expected (code%)", r#"CHR()"#); check_expr_compilation_error("1:14: BOOLEAN is not a number", r#"CHR(FALSE)"#); - check_expr_compilation_error("1:10: CHR expected code%", r#"CHR("a", 1)"#); + check_expr_compilation_error("1:10: CHR expected (code%)", r#"CHR("a", 1)"#); check_expr_error("1:14: Character code -1 must be positive", r#"CHR(-1)"#); check_expr_error("1:14: Invalid character code 55296", r#"CHR(55296)"#); } @@ -741,8 +741,8 @@ mod tests { check_expr_ok_with_vars("abc", r#"LEFT(s, i)"#, [("s", "abcdef".into()), ("i", 3.into())]); - check_expr_compilation_error("1:10: LEFT expected expr$, n%", r#"LEFT()"#); - check_expr_compilation_error("1:10: LEFT expected expr$, n%", r#"LEFT("", 1, 2)"#); + check_expr_compilation_error("1:10: LEFT expected (expr$, n%)", r#"LEFT()"#); + check_expr_compilation_error("1:10: LEFT expected (expr$, n%)", r#"LEFT("", 1, 2)"#); check_expr_compilation_error("1:15: expected STRING but found INTEGER", r#"LEFT(1, 2)"#); check_expr_compilation_error("1:19: STRING is not a number", r#"LEFT("", "")"#); check_expr_error("1:25: n% cannot be negative", r#"LEFT("abcdef", -5)"#); @@ -756,9 +756,9 @@ mod tests { check_expr_ok_with_vars(4, r#"LEN(s)"#, [("s", "1234".into())]); - check_expr_compilation_error("1:10: LEN expected expr$", r#"LEN()"#); + check_expr_compilation_error("1:10: LEN expected (expr$)", r#"LEN()"#); check_expr_compilation_error("1:14: expected STRING but found INTEGER", r#"LEN(3)"#); - check_expr_compilation_error("1:10: LEN expected expr$", r#"LEN(" ", 1)"#); + check_expr_compilation_error("1:10: LEN expected (expr$)", r#"LEN(" ", 1)"#); } #[test] @@ -770,9 +770,9 @@ mod tests { check_expr_ok_with_vars("foo ", r#"LTRIM(s)"#, [("s", " foo ".into())]); - check_expr_compilation_error("1:10: LTRIM expected expr$", r#"LTRIM()"#); + check_expr_compilation_error("1:10: LTRIM expected (expr$)", r#"LTRIM()"#); check_expr_compilation_error("1:16: expected STRING but found INTEGER", r#"LTRIM(3)"#); - check_expr_compilation_error("1:10: LTRIM expected expr$", r#"LTRIM(" ", 1)"#); + check_expr_compilation_error("1:10: LTRIM expected (expr$)", r#"LTRIM(" ", 1)"#); } #[test] @@ -795,15 +795,15 @@ mod tests { ); check_expr_compilation_error( - "1:10: MID expected | ", + "1:10: MID expected (expr$, start%) | (expr$, start%, length%)", r#"MID()"#, ); check_expr_compilation_error( - "1:10: MID expected | ", + "1:10: MID expected (expr$, start%) | (expr$, start%, length%)", r#"MID(3)"#, ); check_expr_compilation_error( - "1:10: MID expected | ", + "1:10: MID expected (expr$, start%) | (expr$, start%, length%)", r#"MID(" ", 1, 1, 10)"#, ); check_expr_compilation_error("1:19: STRING is not a number", r#"MID(" ", "1", 2)"#); @@ -822,8 +822,8 @@ mod tests { check_expr_ok_with_vars("def", r#"RIGHT(s, i)"#, [("s", "abcdef".into()), ("i", 3.into())]); - check_expr_compilation_error("1:10: RIGHT expected expr$, n%", r#"RIGHT()"#); - check_expr_compilation_error("1:10: RIGHT expected expr$, n%", r#"RIGHT("", 1, 2)"#); + check_expr_compilation_error("1:10: RIGHT expected (expr$, n%)", r#"RIGHT()"#); + check_expr_compilation_error("1:10: RIGHT expected (expr$, n%)", r#"RIGHT("", 1, 2)"#); check_expr_compilation_error("1:16: expected STRING but found INTEGER", r#"RIGHT(1, 2)"#); check_expr_compilation_error("1:20: STRING is not a number", r#"RIGHT("", "")"#); check_expr_error("1:26: n% cannot be negative", r#"RIGHT("abcdef", -5)"#); @@ -838,9 +838,9 @@ mod tests { check_expr_ok_with_vars(" foo", r#"RTRIM(s)"#, [("s", " foo ".into())]); - check_expr_compilation_error("1:10: RTRIM expected expr$", r#"RTRIM()"#); + check_expr_compilation_error("1:10: RTRIM expected (expr$)", r#"RTRIM()"#); check_expr_compilation_error("1:16: expected STRING but found INTEGER", r#"RTRIM(3)"#); - check_expr_compilation_error("1:10: RTRIM expected expr$", r#"RTRIM(" ", 1)"#); + check_expr_compilation_error("1:10: RTRIM expected (expr$)", r#"RTRIM(" ", 1)"#); } #[test] @@ -862,8 +862,8 @@ mod tests { check_expr_ok_with_vars(" 1", r#"STR(i)"#, [("i", 1.into())]); - check_expr_compilation_error("1:10: STR expected expr", r#"STR()"#); - check_expr_compilation_error("1:10: STR expected expr", r#"STR(" ", 1)"#); + check_expr_compilation_error("1:10: STR expected (expr)", r#"STR()"#); + check_expr_compilation_error("1:10: STR expected (expr)", r#"STR(" ", 1)"#); } #[test] diff --git a/std/tests/script.bas b/std/tests/script.bas index 3b6e1a33..ce63286b 100644 --- a/std/tests/script.bas +++ b/std/tests/script.bas @@ -16,4 +16,4 @@ ' A sample program to test the scripting interpreter. RANDOMIZE 10 -PRINT "The random number is:"; INT(RND() * 100.0) +PRINT "The random number is:"; INT(RND * 100.0)