From fb34b79c8d870058f87d00dc1abbb85528fc5b6f Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Fri, 9 Jan 2026 16:22:49 -0800 Subject: [PATCH 01/13] Simplify upcall execution Remove the function/builtin call duality when invoking upcalls. There is no need for it other than for debug consistency checks, but we can do those in a simpler way too. --- core/src/exec.rs | 124 ++++++++++------------------------------------- 1 file changed, 25 insertions(+), 99 deletions(-) diff --git a/core/src/exec.rs b/core/src/exec.rs index cd4d6067..de9b4abc 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) { @@ -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)?, From a63457132be02ce5ed0f4838cbc1bb9d7f061c9a Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Fri, 9 Jan 2026 19:14:17 -0800 Subject: [PATCH 02/13] Fix argless syntax Functions that may optionally receive no arguments should not accept nor require empty parenthesis for compatibility with other BASIC dialects. In particular, this means that RND() is now invalid and that the correct syntax is either RND or RND(1). --- NEWS.md | 5 +++ cli/tests/lang/types.out | 2 +- cli/tests/repl/help.out | 64 ++++++++++++++++++------------------ core/src/compiler/exprs.rs | 66 ++++++++++++++++++++++++++++++++------ core/src/compiler/mod.rs | 2 +- core/src/exec.rs | 2 +- core/src/syms.rs | 35 ++++++++++++-------- std/src/arrays.rs | 4 +-- std/src/console/cmds.rs | 21 ++++++------ std/src/gfx/mod.rs | 5 ++- std/src/gpio/mod.rs | 14 +++++--- std/src/help.rs | 48 +++++++++------------------ std/src/numerics.rs | 38 +++++++++++----------- std/src/program.rs | 2 +- std/src/storage/cmds.rs | 9 ++++-- std/src/strings.rs | 38 +++++++++++----------- std/tests/script.bas | 2 +- 17 files changed, 205 insertions(+), 152 deletions(-) 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/compiler/exprs.rs b/core/src/compiler/exprs.rs index a6c5caea..e2777bc9 100644 --- a/core/src/compiler/exprs.rs +++ b/core/src/compiler/exprs.rs @@ -720,13 +720,12 @@ 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, @@ -752,12 +751,12 @@ 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) @@ -863,6 +862,55 @@ mod tests { .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"))) + .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"))) + .check(); + } + #[test] fn test_compile_expr_argless_call_not_argless() { Tester::default() @@ -882,7 +930,7 @@ mod tests { ) .parse("i = f") .compile() - .expect_err("1:5: F expected i%") + .expect_err("1:5: F expected (i%)") .check(); } diff --git a/core/src/compiler/mod.rs b/core/src/compiler/mod.rs index 7dbd1cad..76faa983 100644 --- a/core/src/compiler/mod.rs +++ b/core/src/compiler/mod.rs @@ -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}")] diff --git a/core/src/exec.rs b/core/src/exec.rs index de9b4abc..db1513d8 100644 --- a/core/src/exec.rs +++ b/core/src/exec.rs @@ -3401,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) From 2eb37ef8348c11daa1d707117a02b8a3f2b295a0 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Sat, 10 Jan 2026 09:04:14 -0800 Subject: [PATCH 03/13] Move SymbolsTable to its own file --- core/src/compiler/mod.rs | 188 +------------------------------ core/src/compiler/symtable.rs | 202 ++++++++++++++++++++++++++++++++++ 2 files changed, 205 insertions(+), 185 deletions(-) create mode 100644 core/src/compiler/symtable.rs diff --git a/core/src/compiler/mod.rs b/core/src/compiler/mod.rs index 76faa983..e38c5028 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)] @@ -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 { diff --git a/core/src/compiler/symtable.rs b/core/src/compiler/symtable.rs new file mode 100644 index 00000000..c0a6a705 --- /dev/null +++ b/core/src/compiler/symtable.rs @@ -0,0 +1,202 @@ +// 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}; +use std::collections::HashMap; +#[cfg(test)] +use std::collections::HashSet; + +/// 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, 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. +pub(super) 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. + pub(super) fn enter_scope(&mut self) { + self.scopes.push(HashMap::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`. + 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) + } + + /// 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) { + 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)] + pub(super) 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. + pub(super) 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`. + 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 { + 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() + } +} From fd37895797610f69a7638a43272fac44dc3e282d Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Sat, 10 Jan 2026 09:00:59 -0800 Subject: [PATCH 04/13] Generalize index assignment for builtin upcalls Introduce a new IndexedHashMap type that wraps HashMap and extends it to assign indexes on insertion, allowing those indexes to be retrieved later on a key basis or by extracting the list of keys in insertion order. This is more complicated than it should be just for builtin upcalls but it will be used later once I implement indexes for variables and arrays as well. --- core/src/compiler/args.rs | 2 +- core/src/compiler/exprs.rs | 24 ++-- core/src/compiler/mod.rs | 8 +- core/src/compiler/symtable.rs | 250 ++++++++++++++++++++++++++++------ 4 files changed, 229 insertions(+), 55 deletions(-) diff --git a/core/src/compiler/args.rs b/core/src/compiler/args.rs index 9c74204c..d5dc5f1e 100644 --- a/core/src/compiler/args.rs +++ b/core/src/compiler/args.rs @@ -393,7 +393,7 @@ fn compile_required_ref( Ok(None) } - Some(SymbolPrototype::BuiltinCallable(md, _)) + Some(SymbolPrototype::BuiltinCallable(md)) | Some(SymbolPrototype::Callable(md)) => { if !span.vref.accepts_callable(md.return_type()) { return Err(Error::IncompatibleTypeAnnotationInReference( diff --git a/core/src/compiler/exprs.rs b/core/src/compiler/exprs.rs index e2777bc9..aadfe894 100644 --- a/core/src/compiler/exprs.rs +++ b/core/src/compiler/exprs.rs @@ -353,10 +353,10 @@ 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) } else { @@ -364,7 +364,7 @@ fn compile_expr_symbol( } } - Some(SymbolPrototype::Variable(vtype)) => { + Some((SymbolPrototype::Variable(vtype), _index)) => { if allow_varrefs { (Instruction::LoadRef(key, *vtype, span.pos), *vtype) } else { @@ -378,7 +378,7 @@ fn compile_expr_symbol( } } - Some(SymbolPrototype::BuiltinCallable(md, upcall_index)) => { + Some((SymbolPrototype::BuiltinCallable(md), upcall_index)) => { let etype = match md.return_type() { Some(etype) => etype, None => { @@ -396,7 +396,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 +404,7 @@ fn compile_expr_symbol( ) } - Some(SymbolPrototype::Callable(md)) => { + Some((SymbolPrototype::Callable(md), _index)) => { let etype = match md.return_type() { Some(etype) => etype, None => { @@ -700,12 +700,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)) => { + match symtable.get_with_index(&key) { + Some((SymbolPrototype::Array(vtype, dims), _index)) => { compile_array_ref(instrs, fixups, symtable, span, key, *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, @@ -729,14 +729,14 @@ pub(super) fn compile_expr( 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, @@ -762,7 +762,7 @@ pub(super) fn compile_expr( Ok(vtype) } - Some(SymbolPrototype::Variable(_)) => { + Some((SymbolPrototype::Variable(_), _index)) => { Err(Error::NotArrayOrFunction(span.vref_pos, key)) } diff --git a/core/src/compiler/mod.rs b/core/src/compiler/mod.rs index e38c5028..83c41ef5 100644 --- a/core/src/compiler/mod.rs +++ b/core/src/compiler/mod.rs @@ -713,15 +713,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)); } diff --git a/core/src/compiler/symtable.rs b/core/src/compiler/symtable.rs index c0a6a705..8cf1b426 100644 --- a/core/src/compiler/symtable.rs +++ b/core/src/compiler/symtable.rs @@ -15,9 +15,25 @@ 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)] @@ -27,7 +43,7 @@ pub(super) enum SymbolPrototype { /// 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), + BuiltinCallable(CallableMetadata), /// Information about a callable. Callable(CallableMetadata), @@ -36,6 +52,109 @@ pub(super) enum SymbolPrototype { Variable(ExprType), } +/// A bucketizer for `SymbolPrototype`. +#[derive(Default)] +struct SymbolPrototypeBucketizer {} + +impl SymbolPrototypeBucketizer { + const BUCKET_BUILTINS: u8 = 0; + 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, + _ => 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() } + } + + /// 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`. + 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. + fn insert(&mut self, key: K, value: V) { + 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"); + } + + /// 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 @@ -51,28 +170,31 @@ pub(super) enum SymbolPrototype { /// always have size of one or two. pub(super) struct SymbolsTable { /// Map of global symbol names to their definitions. - globals: HashMap, + globals: SymbolsMap, /// Map of local symbol names to their definitions. - scopes: Vec>, + scopes: Vec, } impl Default for SymbolsTable { fn default() -> Self { - Self { globals: HashMap::default(), scopes: vec![HashMap::default()] } + Self { + globals: IndexedHashMap::new(SymbolPrototypeBucketizer::default()), + scopes: vec![IndexedHashMap::new(SymbolPrototypeBucketizer::default())], + } } } -impl From> for SymbolsTable { - fn from(globals: HashMap) -> Self { - Self { globals, scopes: vec![HashMap::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 = HashMap::default(); + let mut globals = IndexedHashMap::new(SymbolPrototypeBucketizer::default()); let callables = syms.callables(); let mut names = callables.keys().copied().collect::>(); @@ -81,16 +203,16 @@ impl From<&Symbols> for SymbolsTable { // different compilations. names.sort(); - for (i, name) in names.into_iter().enumerate() { + for name in names { let callable = callables.get(&name).unwrap(); - let proto = SymbolPrototype::BuiltinCallable(callable.metadata().clone(), i); + let proto = SymbolPrototype::BuiltinCallable(callable.metadata().clone()); globals.insert(name.clone(), proto); } globals }; - let mut scope = HashMap::default(); + let mut scope = IndexedHashMap::new(SymbolPrototypeBucketizer::default()); for (name, symbol) in syms.locals() { let proto = match symbol { Symbol::Array(array) => { @@ -111,7 +233,7 @@ impl From<&Symbols> for SymbolsTable { impl SymbolsTable { /// Enters a new scope. pub(super) fn enter_scope(&mut self) { - self.scopes.push(HashMap::default()); + self.scopes.push(IndexedHashMap::new(SymbolPrototypeBucketizer::default())); } /// Leaves the current scope. @@ -136,35 +258,36 @@ impl SymbolsTable { 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) { 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"); + 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) { - 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"); + 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) { - let previous = self.globals.insert(key, proto); - debug_assert!(previous.is_none(), "Cannot redefine a symbol"); + self.globals.insert(key, proto); } /// Removes information about the symbol `key`. @@ -185,18 +308,69 @@ impl SymbolsTable { /// Calculates the list of upcalls in this symbols table in the order in which they were /// assigned indexes. pub(super) 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() + 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() + ); } } From 775a02773af5c4a661268645fa9159293e277659 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Sun, 11 Jan 2026 06:19:04 -0800 Subject: [PATCH 05/13] Sort ISpan type definitions --- core/src/bytecode.rs | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/core/src/bytecode.rs b/core/src/bytecode.rs index ac36291e..77626692 100644 --- a/core/src/bytecode.rs +++ b/core/src/bytecode.rs @@ -79,6 +79,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))] @@ -116,20 +130,6 @@ pub struct JumpIfDefinedISpan { pub addr: Address, } -/// 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 request to unset a variable. #[cfg_attr(test, derive(Debug, Eq, PartialEq))] pub struct UnsetISpan { From 79b1366f95330bdd4be77368d9e177bd86a61cb1 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Sun, 11 Jan 2026 06:11:42 -0800 Subject: [PATCH 06/13] Add the ArrayIndexISpan Move the "long" list of arguments in the ArrayAssignment and ArrayLoad operations into an ISpan so that, when I add a runtime index to them soon, things are less confusing. --- core/src/bytecode.rs | 34 +++++++++++++++++-------- core/src/compiler/exprs.rs | 33 +++++++++++++++++++++--- core/src/compiler/mod.rs | 51 +++++++++++++++++++++++++++++++++----- core/src/exec.rs | 8 +++--- 4 files changed, 102 insertions(+), 24 deletions(-) diff --git a/core/src/bytecode.rs b/core/src/bytecode.rs index 77626692..a9fdc9f5 100644 --- a/core/src/bytecode.rs +++ b/core/src/bytecode.rs @@ -22,6 +22,20 @@ 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, + + /// 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))] @@ -279,10 +293,10 @@ 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), @@ -429,12 +443,12 @@ 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())), @@ -585,8 +599,8 @@ impl Instruction { Instruction::ConcatStrings(pos) => Some(*pos), - Instruction::ArrayAssignment(_, pos, _) => Some(*pos), - Instruction::ArrayLoad(_, pos, _) => Some(*pos), + 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, @@ -633,7 +647,7 @@ impl Instruction { | Instruction::BitwiseOr(_) | Instruction::BitwiseXor(_) | Instruction::BitwiseNot(_) - | Instruction::ArrayLoad(_, _, _) + | Instruction::ArrayLoad(..) | Instruction::ShiftLeft(_) | Instruction::ShiftRight(_) | Instruction::EqualBooleans(_) @@ -686,7 +700,7 @@ impl Instruction { | Instruction::EnterScope | Instruction::LeaveScope => false, - Instruction::ArrayAssignment(_, _, _) + Instruction::ArrayAssignment(..) | Instruction::Assign(_) | Instruction::BuiltinCall(_) | Instruction::Call(_) diff --git a/core/src/compiler/exprs.rs b/core/src/compiler/exprs.rs index aadfe894..de9f1d3e 100644 --- a/core/src/compiler/exprs.rs +++ b/core/src/compiler/exprs.rs @@ -446,7 +446,11 @@ 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, + nargs, + })); Ok(vtype) } @@ -1274,7 +1278,14 @@ mod tests { .expect_instr(2, Instruction::AddIntegers(lc(1, 17))) .expect_instr(3, Instruction::LoadInteger(SymbolKey::from("j"), lc(1, 12))) .expect_instr(4, Instruction::PushInteger(3, lc(1, 9))) - .expect_instr(5, Instruction::ArrayLoad(SymbolKey::from("foo"), lc(1, 5), 3)) + .expect_instr( + 5, + Instruction::ArrayLoad(ArrayIndexISpan { + name: SymbolKey::from("foo"), + name_pos: lc(1, 5), + nargs: 3, + }), + ) .expect_instr(6, Instruction::Assign(SymbolKey::from("i"))) .check(); } @@ -1286,7 +1297,14 @@ 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( + 1, + Instruction::ArrayLoad(ArrayIndexISpan { + name: SymbolKey::from("foo"), + name_pos: lc(1, 5), + nargs: 1, + }), + ) .expect_instr(2, Instruction::Assign(SymbolKey::from("i"))) .check(); } @@ -1309,7 +1327,14 @@ 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( + 2, + Instruction::ArrayLoad(ArrayIndexISpan { + name: SymbolKey::from("foo"), + name_pos: lc(1, 5), + nargs: 1, + }), + ) .expect_instr(3, Instruction::Assign(SymbolKey::from("i"))) .check(); } diff --git a/core/src/compiler/mod.rs b/core/src/compiler/mod.rs index 83c41ef5..2fab4c98 100644 --- a/core/src/compiler/mod.rs +++ b/core/src/compiler/mod.rs @@ -297,7 +297,11 @@ 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, + nargs, + })); Ok(()) } @@ -1266,7 +1270,14 @@ mod tests { .expect_instr(3, Instruction::LoadInteger(SymbolKey::from("i"), lc(1, 12))) .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), + nargs: 3, + }), + ) .check(); } @@ -1278,7 +1289,14 @@ 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), + nargs: 1, + }), + ) .check(); } @@ -1291,7 +1309,14 @@ 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), + nargs: 1, + }), + ) .check(); } @@ -1325,7 +1350,14 @@ mod tests { .expect_instr(0, Instruction::LoadDouble(SymbolKey::from("d"), lc(1, 8))) .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), + nargs: 1, + }), + ) .check(); } @@ -1339,7 +1371,14 @@ mod tests { .expect_instr(0, Instruction::LoadInteger(SymbolKey::from("i"), lc(1, 8))) .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), + nargs: 1, + }), + ) .check(); } diff --git a/core/src/exec.rs b/core/src/exec.rs index db1513d8..8d9c390a 100644 --- a/core/src/exec.rs +++ b/core/src/exec.rs @@ -1191,13 +1191,13 @@ impl Machine { 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; } From 248f2ad8f07d3d305d0ca5a99624d6f230e3ad56 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Sun, 11 Jan 2026 06:26:45 -0800 Subject: [PATCH 07/13] Add the LoadSpan Group common arguments in all Load operations under a LoadSpan to make room for other arguments later on. --- core/src/bytecode.rs | 50 ++++--- core/src/compiler/args.rs | 50 +++++-- core/src/compiler/exprs.rs | 65 ++++++--- core/src/compiler/mod.rs | 279 ++++++++++++++++++++++++++++++------- core/src/exec.rs | 28 ++-- 5 files changed, 364 insertions(+), 108 deletions(-) diff --git a/core/src/bytecode.rs b/core/src/bytecode.rs index a9fdc9f5..53bfa0e5 100644 --- a/core/src/bytecode.rs +++ b/core/src/bytecode.rs @@ -144,6 +144,16 @@ pub struct JumpIfDefinedISpan { pub addr: Address, } +/// Components of a load operation. +#[cfg_attr(test, derive(Debug, Eq, PartialEq))] +pub struct LoadISpan { + /// Name of the variable to load. + pub name: SymbolKey, + + /// Position of where this instruction was requested. + pub pos: LineCol, +} + /// Components of a request to unset a variable. #[cfg_attr(test, derive(Debug, Eq, PartialEq))] pub struct UnsetISpan { @@ -345,19 +355,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, @@ -513,12 +523,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), @@ -616,11 +626,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), @@ -688,11 +698,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(_, _) diff --git a/core/src/compiler/args.rs b/core/src/compiler/args.rs index d5dc5f1e..2e988bdc 100644 --- a/core/src/compiler/args.rs +++ b/core/src/compiler/args.rs @@ -355,7 +355,10 @@ fn compile_required_ref( )); } - instrs.push(Instruction::LoadRef(key.clone(), vtype, span.pos)); + instrs.push(Instruction::LoadRef( + LoadISpan { name: key.clone(), pos: span.pos }, + vtype, + )); Ok(Some((key, SymbolPrototype::Variable(vtype)))) } @@ -372,7 +375,10 @@ 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 }, + vtype, + )); Ok(None) } @@ -389,7 +395,10 @@ 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 }, + vtype, + )); Ok(None) } @@ -1192,7 +1201,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) }, + ExprType::Text, + )) .exp_nargs(1) .check(); } @@ -1325,7 +1337,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) }, + ExprType::Integer, + )) .exp_nargs(1) .exp_symbol("foo", ExprType::Integer) .check(); @@ -1353,7 +1368,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) }, + ExprType::Text, + )) .exp_nargs(1) .exp_symbol("foo", ExprType::Text) .check(); @@ -1401,8 +1419,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) }, + ExprType::Integer, + )) + .exp_instr(Instruction::LoadRef( + LoadISpan { name: SymbolKey::from("foo"), pos: lc(1, 2) }, + ExprType::Integer, + )) .exp_nargs(2) .exp_symbol("foo", ExprType::Integer) .check(); @@ -1431,7 +1455,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) }, + ExprType::Text, + )) .exp_nargs(1) .check(); } @@ -1964,7 +1991,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) }, + ExprType::Text, + )) .exp_nargs(1) .check(); } diff --git a/core/src/compiler/exprs.rs b/core/src/compiler/exprs.rs index de9f1d3e..c36a5f61 100644 --- a/core/src/compiler/exprs.rs +++ b/core/src/compiler/exprs.rs @@ -358,7 +358,7 @@ fn compile_expr_symbol( Some((SymbolPrototype::Array(atype, _dims), _index)) => { if allow_varrefs { - (Instruction::LoadRef(key, *atype, span.pos), *atype) + (Instruction::LoadRef(LoadISpan { name: key, pos: span.pos }, *atype), *atype) } else { return Err(Error::NotAVariable(span.pos, span.vref)); } @@ -366,7 +366,7 @@ fn compile_expr_symbol( Some((SymbolPrototype::Variable(vtype), _index)) => { if allow_varrefs { - (Instruction::LoadRef(key, *vtype, span.pos), *vtype) + (Instruction::LoadRef(LoadISpan { name: key, pos: span.pos }, *vtype), *vtype) } else { let instr = match vtype { ExprType::Boolean => Instruction::LoadBoolean, @@ -374,7 +374,7 @@ fn compile_expr_symbol( ExprType::Integer => Instruction::LoadInteger, ExprType::Text => Instruction::LoadString, }; - (instr(key, span.pos), *vtype) + (instr(LoadISpan { name: key, pos: span.pos }), *vtype) } } @@ -841,7 +841,10 @@ 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( + 0, + Instruction::LoadInteger(LoadISpan { name: SymbolKey::from("j"), pos: lc(1, 5) }), + ) .expect_instr(1, Instruction::Assign(SymbolKey::from("i"))) .check(); } @@ -1048,7 +1051,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) }, + ExprType::Integer, + ), ) .expect_instr( 1, @@ -1101,11 +1107,20 @@ 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) }), + ) .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) }), + ) .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) }), + ) .expect_instr(6, Instruction::LogicalNot(lc(1, 25))) .expect_instr(7, Instruction::LogicalXor(lc(1, 21))) .expect_instr(8, Instruction::Assign(SymbolKey::from("b"))) @@ -1119,10 +1134,16 @@ 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) }), + ) .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) }), + ) .expect_instr(4, Instruction::ShiftLeft(lc(1, 12))) .expect_instr(5, Instruction::Assign(SymbolKey::from("i"))) .check(); @@ -1140,7 +1161,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, @@ -1157,7 +1178,7 @@ 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) })) .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"))) @@ -1273,10 +1294,16 @@ 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) }), + ) .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) }), + ) .expect_instr(4, Instruction::PushInteger(3, lc(1, 9))) .expect_instr( 5, @@ -1395,12 +1422,18 @@ 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) }), + ) .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) }), + ) .expect_instr(6, Instruction::PushInteger(3, lc(1, 9))) .expect_instr( 7, diff --git a/core/src/compiler/mod.rs b/core/src/compiler/mod.rs index 2fab4c98..6dee799c 100644 --- a/core/src/compiler/mod.rs +++ b/core/src/compiler/mod.rs @@ -967,7 +967,10 @@ 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, + })); self.emit(Instruction::LeaveScope); self.symtable.leave_scope(); @@ -1265,9 +1268,15 @@ 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) }), + ) .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) }), + ) .expect_instr(4, Instruction::AddIntegers(lc(1, 10))) .expect_instr(5, Instruction::PushInteger(3, lc(1, 5))) .expect_instr( @@ -1347,7 +1356,10 @@ 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) }), + ) .expect_instr(1, Instruction::DoubleToInteger) .expect_instr(2, Instruction::PushInteger(3, lc(1, 3))) .expect_instr( @@ -1368,7 +1380,10 @@ 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) }), + ) .expect_instr(1, Instruction::IntegerToDouble) .expect_instr(2, Instruction::PushInteger(3, lc(1, 3))) .expect_instr( @@ -1439,7 +1454,10 @@ 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( + 0, + Instruction::LoadInteger(LoadISpan { name: SymbolKey::from("i"), pos: lc(1, 7) }), + ) .expect_instr(1, Instruction::Assign(SymbolKey::from("foo"))) .check(); } @@ -1709,7 +1727,10 @@ 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) }), + ) .expect_instr( 4, Instruction::DimArray(DimArrayISpan { @@ -1861,7 +1882,10 @@ 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) }), + ) .expect_instr(2, Instruction::AddIntegers(lc(1, 7))) .expect_instr(3, Instruction::End(true)) .check(); @@ -1873,7 +1897,10 @@ 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) }), + ) .expect_instr(1, Instruction::End(true)) .check(); } @@ -1990,12 +2017,18 @@ mod tests { .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( + 2, + Instruction::LoadInteger(LoadISpan { name: SymbolKey::from("i"), pos: lc(1, 5) }), + ) .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) }), + ) .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"))) @@ -2010,26 +2043,38 @@ mod tests { .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( + 2, + Instruction::LoadInteger(LoadISpan { name: SymbolKey::from("i"), pos: lc(1, 5) }), + ) .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( + 8, + Instruction::LoadInteger(LoadISpan { name: SymbolKey::from("j"), pos: lc(2, 5) }), + ) .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) }), + ) .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(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) }), + ) .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"))) @@ -2060,7 +2105,10 @@ mod tests { // 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( + 2, + Instruction::LoadInteger(LoadISpan { name: SymbolKey::from("i"), pos: lc(2, 5) }), + ) .expect_instr(3, Instruction::PushInteger(10, lc(2, 14))) .expect_instr(4, Instruction::LessEqualIntegers(lc(2, 11))) .expect_instr(5, Instruction::JumpIfNotTrue(14)) @@ -2069,7 +2117,10 @@ 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) }), + ) .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"))) @@ -2086,13 +2137,25 @@ mod tests { .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( + 2, + Instruction::LoadInteger(LoadISpan { + name: SymbolKey::from("iter"), + pos: lc(1, 5), + }), + ) .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( + 8, + Instruction::LoadInteger(LoadISpan { + name: SymbolKey::from("iter"), + pos: lc(1, 5), + }), + ) .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"))) @@ -2107,15 +2170,33 @@ 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( + 0, + Instruction::LoadInteger(LoadISpan { name: SymbolKey::from("i"), pos: 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( + 2, + Instruction::LoadInteger(LoadISpan { + name: SymbolKey::from("iter"), + pos: lc(1, 5), + }), + ) + .expect_instr( + 3, + Instruction::LoadInteger(LoadISpan { name: SymbolKey::from("j"), pos: 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( + 8, + Instruction::LoadInteger(LoadISpan { + name: SymbolKey::from("iter"), + pos: lc(1, 5), + }), + ) .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"))) @@ -2130,19 +2211,37 @@ 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) }), + ) .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( + 4, + Instruction::LoadInteger(LoadISpan { + name: SymbolKey::from("iter"), + pos: lc(1, 5), + }), + ) .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) }), + ) .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( + 12, + Instruction::LoadInteger(LoadISpan { + name: SymbolKey::from("iter"), + pos: lc(1, 5), + }), + ) .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"))) @@ -2173,12 +2272,18 @@ mod tests { .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( + 5, + Instruction::LoadDouble(LoadISpan { name: SymbolKey::from("iter"), pos: lc(1, 5) }), + ) .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) }), + ) .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"))) @@ -2203,7 +2308,13 @@ mod tests { ) .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( + 5, + Instruction::LoadInteger(LoadISpan { + name: SymbolKey::from("0return_foo"), + pos: lc(1, 22), + }), + ) .expect_instr(6, Instruction::LeaveScope) .expect_instr(7, Instruction::Return(lc(1, 22))) .expect_symtable( @@ -2236,7 +2347,13 @@ mod tests { 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), + }), + ) .expect_instr(8, Instruction::LeaveScope) .expect_instr(9, Instruction::Return(lc(1, 27))) .expect_symtable( @@ -2270,7 +2387,13 @@ mod tests { .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( + 8, + Instruction::LoadInteger(LoadISpan { + name: SymbolKey::from("0return_foo"), + pos: lc(1, 44), + }), + ) .expect_instr(9, Instruction::LeaveScope) .expect_instr(10, Instruction::Return(lc(1, 44))) .expect_symtable( @@ -2579,7 +2702,10 @@ 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), + }), Instruction::PushInteger(1, lc(2, 6)), Instruction::PushInteger(2, lc(2, 10)), Instruction::AddIntegers(lc(2, 8)), @@ -2593,7 +2719,10 @@ 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), + }), Instruction::PushInteger(9, lc(2, 11)), Instruction::PushInteger(8, lc(2, 15)), Instruction::AddIntegers(lc(2, 13)), @@ -2604,7 +2733,10 @@ 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), + }), Instruction::PushInteger(9, lc(2, 12)), Instruction::NotEqualIntegers(lc(2, 12)), ], @@ -2613,7 +2745,10 @@ 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), + }), Instruction::PushInteger(9, lc(2, 11)), Instruction::LessIntegers(lc(2, 11)), ], @@ -2622,7 +2757,10 @@ 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), + }), Instruction::PushInteger(9, lc(2, 12)), Instruction::LessEqualIntegers(lc(2, 12)), ], @@ -2631,7 +2769,10 @@ 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), + }), Instruction::PushInteger(9, lc(2, 11)), Instruction::GreaterIntegers(lc(2, 11)), ], @@ -2640,7 +2781,10 @@ 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), + }), Instruction::PushInteger(9, lc(2, 12)), Instruction::GreaterEqualIntegers(lc(2, 12)), ], @@ -2652,10 +2796,16 @@ 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), + }), 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), + }), Instruction::PushInteger(2, lc(2, 11)), Instruction::LessEqualIntegers(lc(2, 11)), Instruction::LogicalAnd(lc(2, 6)), @@ -2668,10 +2818,16 @@ 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), + }), 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), + }), Instruction::PushInteger(8, lc(2, 15)), Instruction::EqualIntegers(lc(2, 15)), Instruction::LogicalOr(lc(2, 12)), @@ -2706,7 +2862,13 @@ mod tests { .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( + 2, + Instruction::LoadInteger(LoadISpan { + name: SymbolKey::from("0select1"), + pos: lc(2, 6), + }), + ) .expect_instr(3, Instruction::PushInteger(7, lc(2, 6))) .expect_instr(4, Instruction::EqualIntegers(lc(2, 6))) .expect_instr(5, Instruction::JumpIfNotTrue(7)) @@ -2732,7 +2894,10 @@ 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( + 0, + Instruction::LoadInteger(LoadISpan { name: SymbolKey::from("i"), pos: lc(1, 13) }), + ) .expect_instr(1, Instruction::Assign(SymbolKey::from("0select1"))) .expect_instr( 2, @@ -2777,7 +2942,13 @@ mod tests { .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( + 2, + Instruction::LoadInteger(LoadISpan { + name: SymbolKey::from("0select1"), + pos: lc(2, 6), + }), + ) .expect_instr(3, Instruction::PushInteger(7, lc(2, 6))) .expect_instr(4, Instruction::EqualIntegers(lc(2, 6))) .expect_instr(5, Instruction::JumpIfNotTrue(8)) @@ -2791,7 +2962,13 @@ 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), + }), + ) .expect_instr(9, Instruction::PushInteger(8, lc(4, 12))) .expect_instr(10, Instruction::NotEqualIntegers(lc(4, 12))) .expect_instr(11, Instruction::JumpIfNotTrue(13)) @@ -2820,7 +2997,13 @@ mod tests { .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( + 2, + Instruction::LoadInteger(LoadISpan { + name: SymbolKey::from("0select1"), + pos: lc(2, 6), + }), + ) .expect_instr(3, Instruction::PushInteger(7, lc(2, 6))) .expect_instr(4, Instruction::EqualIntegers(lc(2, 6))) .expect_instr(5, Instruction::JumpIfNotTrue(8)) diff --git a/core/src/exec.rs b/core/src/exec.rs index 8d9c390a..8158cdaf 100644 --- a/core/src/exec.rs +++ b/core/src/exec.rs @@ -1315,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; } From 82bc8655feed376877de2f545599abd265dacacc Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Sat, 10 Jan 2026 18:15:31 -0800 Subject: [PATCH 08/13] Assign indexes to dim operations Modify the symbols table so that it assigns indexes to variables and arrays and propagate this information to the Dim and DimArray bytecode operations. This is just the beginning of adding indexes, and the indexes are not yet consumed. --- core/src/bytecode.rs | 6 ++++ core/src/compiler/mod.rs | 58 +++++++++++++++++++++++------------ core/src/compiler/symtable.rs | 15 +++++---- 3 files changed, 54 insertions(+), 25 deletions(-) diff --git a/core/src/bytecode.rs b/core/src/bytecode.rs index 53bfa0e5..eb589eb9 100644 --- a/core/src/bytecode.rs +++ b/core/src/bytecode.rs @@ -63,6 +63,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, @@ -80,6 +83,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, diff --git a/core/src/compiler/mod.rs b/core/src/compiler/mod.rs index 6dee799c..8d641ea2 100644 --- a/core/src/compiler/mod.rs +++ b/core/src/compiler/mod.rs @@ -390,16 +390,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(()) } @@ -474,18 +476,19 @@ 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() { + let index = + self.symtable.insert(key.clone(), SymbolPrototype::Variable(ExprType::Double)); self.emit(Instruction::Dim(DimISpan { - name: key, + name: key.clone(), + index, shared: false, vtype: ExprType::Double, })); - self.symtable.insert(iter_key.clone(), SymbolPrototype::Variable(ExprType::Double)); } self.instrs[skip_pc] = Instruction::JumpIfDefined(JumpIfDefinedISpan { - var: iter_key, + var: key, addr: self.instrs.len(), }); } @@ -783,20 +786,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) => { @@ -941,13 +946,15 @@ 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()); @@ -1589,6 +1596,7 @@ mod tests { 0, Instruction::Dim(DimISpan { name: SymbolKey::from("var"), + index: 0, shared: false, vtype: ExprType::Boolean, }), @@ -1601,6 +1609,7 @@ mod tests { 0, Instruction::Dim(DimISpan { name: SymbolKey::from("var"), + index: 0, shared: false, vtype: ExprType::Double, }), @@ -1613,6 +1622,7 @@ mod tests { 0, Instruction::Dim(DimISpan { name: SymbolKey::from("var"), + index: 0, shared: false, vtype: ExprType::Integer, }), @@ -1625,6 +1635,7 @@ mod tests { 0, Instruction::Dim(DimISpan { name: SymbolKey::from("var"), + index: 0, shared: false, vtype: ExprType::Text, }), @@ -1676,6 +1687,7 @@ mod tests { 0, Instruction::Dim(DimISpan { name: SymbolKey::from("var"), + index: 0, shared: true, vtype: ExprType::Boolean, }), @@ -1709,6 +1721,7 @@ mod tests { Instruction::DimArray(DimArrayISpan { name: SymbolKey::from("var"), name_pos: lc(1, 5), + index: 0, shared: false, dimensions: 1, subtype: ExprType::Integer, @@ -1736,6 +1749,7 @@ mod tests { Instruction::DimArray(DimArrayISpan { name: SymbolKey::from("var"), name_pos: lc(1, 5), + index: 1, shared: false, dimensions: 2, subtype: ExprType::Integer, @@ -1757,6 +1771,7 @@ mod tests { Instruction::DimArray(DimArrayISpan { name: SymbolKey::from("var"), name_pos: lc(1, 5), + index: 0, shared: false, dimensions: 1, subtype: ExprType::Integer, @@ -1786,6 +1801,7 @@ mod tests { Instruction::DimArray(DimArrayISpan { name: SymbolKey::from("var"), name_pos: lc(1, 12), + index: 0, shared: true, dimensions: 1, subtype: ExprType::Integer, @@ -2265,6 +2281,7 @@ mod tests { 1, Instruction::Dim(DimISpan { name: SymbolKey::from("iter"), + index: 0, shared: false, vtype: ExprType::Double, }), @@ -2302,6 +2319,7 @@ mod tests { 2, Instruction::Dim(DimISpan { name: SymbolKey::from("0return_foo"), + index: 0, shared: false, vtype: ExprType::Integer, }), @@ -2343,6 +2361,7 @@ mod tests { 6, Instruction::Dim(DimISpan { name: SymbolKey::from("0return_foo"), + index: 0, shared: false, vtype: ExprType::Integer, }), @@ -2378,6 +2397,7 @@ mod tests { 2, Instruction::Dim(DimISpan { name: SymbolKey::from("0return_foo"), + index: 0, shared: false, vtype: ExprType::Integer, }), diff --git a/core/src/compiler/symtable.rs b/core/src/compiler/symtable.rs index 8cf1b426..90c3d874 100644 --- a/core/src/compiler/symtable.rs +++ b/core/src/compiler/symtable.rs @@ -58,6 +58,7 @@ struct SymbolPrototypeBucketizer {} impl SymbolPrototypeBucketizer { const BUCKET_BUILTINS: u8 = 0; + const BUCKET_STACK: u8 = 1; const BUCKET_OTHER: u8 = 255; } @@ -67,6 +68,7 @@ impl Bucketizer for SymbolPrototypeBucketizer { fn bucketize(&self, value: &SymbolPrototype) -> u8 { match value { SymbolPrototype::BuiltinCallable(..) => Self::BUCKET_BUILTINS, + SymbolPrototype::Array(..) | SymbolPrototype::Variable(..) => Self::BUCKET_STACK, _ => Self::BUCKET_OTHER, } } @@ -112,14 +114,15 @@ impl> IndexedHashMap { } /// Same as `HashMap::insert` but does not return the previous value because this assumes that - /// no previous value can exist. - fn insert(&mut self, key: K, value: V) { + /// 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`. @@ -270,9 +273,9 @@ impl SymbolsTable { /// 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) { + 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); + self.scopes.last_mut().unwrap().insert(key, proto) } /// Inserts the builtin callable described by `md` and assigns an upcall index. @@ -286,8 +289,8 @@ impl SymbolsTable { /// 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) { - self.globals.insert(key, proto); + pub(super) fn insert_global(&mut self, key: SymbolKey, proto: SymbolPrototype) -> usize { + self.globals.insert(key, proto) } /// Removes information about the symbol `key`. From 5ccfd3682299a229deea59d6b21eeea2bb28549d Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Sun, 11 Jan 2026 06:39:42 -0800 Subject: [PATCH 09/13] Assign indexes to array operations --- core/src/bytecode.rs | 3 +++ core/src/compiler/exprs.rs | 10 ++++++++-- core/src/compiler/mod.rs | 10 ++++++++-- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/core/src/bytecode.rs b/core/src/bytecode.rs index eb589eb9..832d8ca4 100644 --- a/core/src/bytecode.rs +++ b/core/src/bytecode.rs @@ -32,6 +32,9 @@ pub struct ArrayIndexISpan { /// 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, } diff --git a/core/src/compiler/exprs.rs b/core/src/compiler/exprs.rs index c36a5f61..39dedc85 100644 --- a/core/src/compiler/exprs.rs +++ b/core/src/compiler/exprs.rs @@ -430,12 +430,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 { @@ -449,6 +451,7 @@ fn compile_array_ref( instrs.push(Instruction::ArrayLoad(ArrayIndexISpan { name: key, name_pos: span.vref_pos, + index, nargs, })); Ok(vtype) @@ -705,8 +708,8 @@ pub(super) fn compile_expr( Expr::Call(span) => { let key = SymbolKey::from(span.vref.name()); match symtable.get_with_index(&key) { - Some((SymbolPrototype::Array(vtype, dims), _index)) => { - compile_array_ref(instrs, fixups, symtable, span, key, *vtype, *dims) + Some((SymbolPrototype::Array(vtype, dims), index)) => { + compile_array_ref(instrs, fixups, symtable, span, key, index, *vtype, *dims) } Some((SymbolPrototype::BuiltinCallable(md), upcall_index)) => { @@ -1310,6 +1313,7 @@ mod tests { Instruction::ArrayLoad(ArrayIndexISpan { name: SymbolKey::from("foo"), name_pos: lc(1, 5), + index: 0, nargs: 3, }), ) @@ -1329,6 +1333,7 @@ mod tests { Instruction::ArrayLoad(ArrayIndexISpan { name: SymbolKey::from("foo"), name_pos: lc(1, 5), + index: 0, nargs: 1, }), ) @@ -1359,6 +1364,7 @@ mod tests { Instruction::ArrayLoad(ArrayIndexISpan { name: SymbolKey::from("foo"), name_pos: lc(1, 5), + index: 0, nargs: 1, }), ) diff --git a/core/src/compiler/mod.rs b/core/src/compiler/mod.rs index 8d641ea2..585a09fc 100644 --- a/core/src/compiler/mod.rs +++ b/core/src/compiler/mod.rs @@ -274,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())); } @@ -300,6 +300,7 @@ impl Compiler { self.emit(Instruction::ArrayAssignment(ArrayIndexISpan { name: key, name_pos: span.vref_pos, + index, nargs, })); @@ -1291,6 +1292,7 @@ mod tests { Instruction::ArrayAssignment(ArrayIndexISpan { name: SymbolKey::from("foo"), name_pos: lc(1, 1), + index: 0, nargs: 3, }), ) @@ -1310,6 +1312,7 @@ mod tests { Instruction::ArrayAssignment(ArrayIndexISpan { name: SymbolKey::from("a"), name_pos: lc(1, 1), + index: 0, nargs: 1, }), ) @@ -1330,6 +1333,7 @@ mod tests { Instruction::ArrayAssignment(ArrayIndexISpan { name: SymbolKey::from("a"), name_pos: lc(1, 1), + index: 0, nargs: 1, }), ) @@ -1374,6 +1378,7 @@ mod tests { Instruction::ArrayAssignment(ArrayIndexISpan { name: SymbolKey::from("a"), name_pos: lc(1, 1), + index: 0, nargs: 1, }), ) @@ -1398,6 +1403,7 @@ mod tests { Instruction::ArrayAssignment(ArrayIndexISpan { name: SymbolKey::from("a"), name_pos: lc(1, 1), + index: 0, nargs: 1, }), ) From 735b82ea58d4de6c0a87dccadcde9f915065bb43 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Sun, 11 Jan 2026 06:39:42 -0800 Subject: [PATCH 10/13] Assign indexes to jump operations --- core/src/bytecode.rs | 3 +++ core/src/compiler/mod.rs | 27 +++++++++++++++++---------- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/core/src/bytecode.rs b/core/src/bytecode.rs index 832d8ca4..c87c9fad 100644 --- a/core/src/bytecode.rs +++ b/core/src/bytecode.rs @@ -149,6 +149,9 @@ 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, } diff --git a/core/src/compiler/mod.rs b/core/src/compiler/mod.rs index 585a09fc..c387c48f 100644 --- a/core/src/compiler/mod.rs +++ b/core/src/compiler/mod.rs @@ -477,19 +477,25 @@ impl Compiler { let key = SymbolKey::from(span.iter.name()); let skip_pc = self.emit(Instruction::Nop); - if self.symtable.get(&key).is_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, - })); - } + 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: key, + index, addr: self.instrs.len(), }); } @@ -2280,6 +2286,7 @@ mod tests { 0, Instruction::JumpIfDefined(JumpIfDefinedISpan { var: SymbolKey::from("iter"), + index: 0, addr: 2, }), ) From 838c811a27dcc717f2f50d352ebf8a18c8c33135 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Sun, 11 Jan 2026 06:52:46 -0800 Subject: [PATCH 11/13] Assign indexes to load operations --- core/src/bytecode.rs | 3 + core/src/compiler/args.rs | 38 ++++---- core/src/compiler/exprs.rs | 83 +++++++++++++---- core/src/compiler/mod.rs | 163 +++++++++++++++++++++++++++++----- core/src/compiler/symtable.rs | 11 +++ 5 files changed, 242 insertions(+), 56 deletions(-) diff --git a/core/src/bytecode.rs b/core/src/bytecode.rs index c87c9fad..497dda1f 100644 --- a/core/src/bytecode.rs +++ b/core/src/bytecode.rs @@ -164,6 +164,9 @@ pub struct LoadISpan { /// Position of where this instruction was requested. pub pos: LineCol, + + /// Index of `name` on the stack. + pub index: usize, } /// Components of a request to unset a variable. diff --git a/core/src/compiler/args.rs b/core/src/compiler/args.rs index 2e988bdc..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,14 +357,16 @@ fn compile_required_ref( )); } + 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 }, + LoadISpan { name: key.clone(), pos: span.pos, index: next_index }, vtype, )); - Ok(Some((key, SymbolPrototype::Variable(vtype)))) + Ok(Some((key, proto))) } - Some(SymbolPrototype::Array(vtype, _)) => { + Some((SymbolPrototype::Array(vtype, _), index)) => { let vtype = *vtype; if !span.vref.accepts(vtype) { @@ -376,13 +380,13 @@ fn compile_required_ref( } instrs.push(Instruction::LoadRef( - LoadISpan { name: key.clone(), pos: span.pos }, + 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) { @@ -396,14 +400,14 @@ fn compile_required_ref( } instrs.push(Instruction::LoadRef( - LoadISpan { name: key.clone(), pos: span.pos }, + 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, @@ -549,6 +553,7 @@ fn compile_args( md, pos, symtable, + to_insert.len(), false, true, Some(expr), @@ -622,6 +627,7 @@ fn compile_args( md, pos, symtable, + 0, details.require_array, details.define_undefined, span.expr, @@ -1202,7 +1208,7 @@ mod compile_tests { sep_pos: lc(1, 5), }]) .exp_instr(Instruction::LoadRef( - LoadISpan { name: SymbolKey::from("foo"), pos: lc(1, 2) }, + LoadISpan { name: SymbolKey::from("foo"), pos: lc(1, 2), index: 0 }, ExprType::Text, )) .exp_nargs(1) @@ -1338,7 +1344,7 @@ mod compile_tests { sep_pos: lc(1, 5), }]) .exp_instr(Instruction::LoadRef( - LoadISpan { name: SymbolKey::from("foo"), pos: lc(1, 2) }, + LoadISpan { name: SymbolKey::from("foo"), pos: lc(1, 2), index: 0 }, ExprType::Integer, )) .exp_nargs(1) @@ -1369,7 +1375,7 @@ mod compile_tests { sep_pos: lc(1, 6), }]) .exp_instr(Instruction::LoadRef( - LoadISpan { name: SymbolKey::from("foo"), pos: lc(1, 2) }, + LoadISpan { name: SymbolKey::from("foo"), pos: lc(1, 2), index: 0 }, ExprType::Text, )) .exp_nargs(1) @@ -1420,11 +1426,11 @@ mod compile_tests { }, ]) .exp_instr(Instruction::LoadRef( - LoadISpan { name: SymbolKey::from("foo"), pos: lc(1, 2) }, + 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) }, + LoadISpan { name: SymbolKey::from("foo"), pos: lc(1, 2), index: 0 }, ExprType::Integer, )) .exp_nargs(2) @@ -1456,7 +1462,7 @@ mod compile_tests { sep_pos: lc(1, 5), }]) .exp_instr(Instruction::LoadRef( - LoadISpan { name: SymbolKey::from("foo"), pos: lc(1, 2) }, + LoadISpan { name: SymbolKey::from("foo"), pos: lc(1, 2), index: 0 }, ExprType::Text, )) .exp_nargs(1) @@ -1992,7 +1998,7 @@ mod compile_tests { sep_pos: lc(1, 2), }]) .exp_instr(Instruction::LoadRef( - LoadISpan { name: SymbolKey::from("foo"), pos: lc(1, 2) }, + LoadISpan { name: SymbolKey::from("foo"), pos: lc(1, 2), index: 0 }, ExprType::Text, )) .exp_nargs(1) diff --git a/core/src/compiler/exprs.rs b/core/src/compiler/exprs.rs index 39dedc85..9a567705 100644 --- a/core/src/compiler/exprs.rs +++ b/core/src/compiler/exprs.rs @@ -356,17 +356,23 @@ fn compile_expr_symbol( let (instr, vtype) = match symtable.get_with_index(&key) { None => return Err(Error::UndefinedSymbol(span.pos, key)), - Some((SymbolPrototype::Array(atype, _dims), _index)) => { + Some((SymbolPrototype::Array(atype, _dims), index)) => { if allow_varrefs { - (Instruction::LoadRef(LoadISpan { name: key, pos: span.pos }, *atype), *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), _index)) => { + Some((SymbolPrototype::Variable(vtype), index)) => { if allow_varrefs { - (Instruction::LoadRef(LoadISpan { name: key, pos: span.pos }, *vtype), *vtype) + ( + Instruction::LoadRef(LoadISpan { name: key, pos: span.pos, index }, *vtype), + *vtype, + ) } else { let instr = match vtype { ExprType::Boolean => Instruction::LoadBoolean, @@ -374,7 +380,7 @@ fn compile_expr_symbol( ExprType::Integer => Instruction::LoadInteger, ExprType::Text => Instruction::LoadString, }; - (instr(LoadISpan { name: key, pos: span.pos }), *vtype) + (instr(LoadISpan { name: key, pos: span.pos, index }), *vtype) } } @@ -846,7 +852,11 @@ mod tests { .compile() .expect_instr( 0, - Instruction::LoadInteger(LoadISpan { name: SymbolKey::from("j"), pos: lc(1, 5) }), + Instruction::LoadInteger(LoadISpan { + name: SymbolKey::from("j"), + pos: lc(1, 5), + index: 0, + }), ) .expect_instr(1, Instruction::Assign(SymbolKey::from("i"))) .check(); @@ -1055,7 +1065,7 @@ mod tests { .expect_instr( 0, Instruction::LoadRef( - LoadISpan { name: SymbolKey::from("a"), pos: lc(1, 3) }, + LoadISpan { name: SymbolKey::from("a"), pos: lc(1, 3), index: 0 }, ExprType::Integer, ), ) @@ -1112,17 +1122,29 @@ mod tests { .expect_instr(0, Instruction::PushBoolean(true, lc(1, 5))) .expect_instr( 1, - Instruction::LoadBoolean(LoadISpan { name: SymbolKey::from("x"), pos: lc(1, 13) }), + 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(LoadISpan { name: SymbolKey::from("y"), pos: lc(1, 19) }), + 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(LoadISpan { name: SymbolKey::from("z"), pos: lc(1, 29) }), + 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))) @@ -1139,13 +1161,21 @@ mod tests { .compile() .expect_instr( 0, - Instruction::LoadInteger(LoadISpan { name: SymbolKey::from("a"), pos: lc(1, 5) }), + 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(LoadISpan { name: SymbolKey::from("b"), pos: lc(1, 15) }), + 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"))) @@ -1181,7 +1211,10 @@ mod tests { .define("a", SymbolPrototype::Variable(test_value_type)) .parse(&format!("b = a {} {}", op_name, test_value_str)) .compile() - .expect_instr(0, load_inst(LoadISpan { name: SymbolKey::from("a"), pos: 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"))) @@ -1299,13 +1332,21 @@ mod tests { .compile() .expect_instr( 0, - Instruction::LoadInteger(LoadISpan { name: SymbolKey::from("k"), pos: lc(1, 15) }), + 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(LoadISpan { name: SymbolKey::from("j"), pos: lc(1, 12) }), + Instruction::LoadInteger(LoadISpan { + name: SymbolKey::from("j"), + pos: lc(1, 12), + index: 1, + }), ) .expect_instr(4, Instruction::PushInteger(3, lc(1, 9))) .expect_instr( @@ -1430,7 +1471,11 @@ mod tests { .compile() .expect_instr( 0, - Instruction::LoadDouble(LoadISpan { name: SymbolKey::from("k"), pos: lc(1, 15) }), + 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) @@ -1438,7 +1483,11 @@ mod tests { .expect_instr(4, Instruction::DoubleToInteger) .expect_instr( 5, - Instruction::LoadInteger(LoadISpan { name: SymbolKey::from("j"), pos: lc(1, 12) }), + Instruction::LoadInteger(LoadISpan { + name: SymbolKey::from("j"), + pos: lc(1, 12), + index: 0, + }), ) .expect_instr(6, Instruction::PushInteger(3, lc(1, 9))) .expect_instr( diff --git a/core/src/compiler/mod.rs b/core/src/compiler/mod.rs index c387c48f..a5f69884 100644 --- a/core/src/compiler/mod.rs +++ b/core/src/compiler/mod.rs @@ -984,6 +984,7 @@ impl Compiler { let epilogue_pc = self.emit(load_inst(LoadISpan { name: return_value.clone(), pos: span.end_pos, + index, })); self.emit(Instruction::LeaveScope); @@ -1284,12 +1285,20 @@ mod tests { .expect_instr(0, Instruction::PushInteger(5, lc(1, 20))) .expect_instr( 1, - Instruction::LoadInteger(LoadISpan { name: SymbolKey::from("i"), pos: lc(1, 15) }), + 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(LoadISpan { name: SymbolKey::from("i"), pos: lc(1, 12) }), + 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))) @@ -1375,7 +1384,11 @@ mod tests { .compile() .expect_instr( 0, - Instruction::LoadDouble(LoadISpan { name: SymbolKey::from("d"), pos: lc(1, 8) }), + 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))) @@ -1400,7 +1413,11 @@ mod tests { .compile() .expect_instr( 0, - Instruction::LoadInteger(LoadISpan { name: SymbolKey::from("i"), pos: lc(1, 8) }), + 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))) @@ -1475,7 +1492,11 @@ mod tests { .compile() .expect_instr( 0, - Instruction::LoadInteger(LoadISpan { name: SymbolKey::from("i"), pos: lc(1, 7) }), + Instruction::LoadInteger(LoadISpan { + name: SymbolKey::from("i"), + pos: lc(1, 7), + index: 0, + }), ) .expect_instr(1, Instruction::Assign(SymbolKey::from("foo"))) .check(); @@ -1754,7 +1775,11 @@ mod tests { .expect_instr(2, Instruction::AddIntegers(lc(1, 14))) .expect_instr( 3, - Instruction::LoadInteger(LoadISpan { name: SymbolKey::from("i"), pos: lc(1, 9) }), + Instruction::LoadInteger(LoadISpan { + name: SymbolKey::from("i"), + pos: lc(1, 9), + index: 0, + }), ) .expect_instr( 4, @@ -1912,7 +1937,11 @@ mod tests { .expect_instr(0, Instruction::PushInteger(2, lc(1, 5))) .expect_instr( 1, - Instruction::LoadInteger(LoadISpan { name: SymbolKey::from("i"), pos: lc(1, 9) }), + 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)) @@ -1927,7 +1956,11 @@ mod tests { .compile() .expect_instr( 0, - Instruction::LoadInteger(LoadISpan { name: SymbolKey::from("i"), pos: lc(1, 5) }), + Instruction::LoadInteger(LoadISpan { + name: SymbolKey::from("i"), + pos: lc(1, 5), + index: 0, + }), ) .expect_instr(1, Instruction::End(true)) .check(); @@ -2047,7 +2080,11 @@ mod tests { .expect_instr(1, Instruction::Assign(SymbolKey::from("i"))) .expect_instr( 2, - Instruction::LoadInteger(LoadISpan { name: SymbolKey::from("i"), pos: lc(1, 5) }), + 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))) @@ -2055,7 +2092,11 @@ mod tests { .expect_instr(6, Instruction::Jump(JumpISpan { addr: 12 })) .expect_instr( 7, - Instruction::LoadInteger(LoadISpan { name: SymbolKey::from("i"), pos: lc(1, 5) }), + 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))) @@ -2073,7 +2114,11 @@ mod tests { .expect_instr(1, Instruction::Assign(SymbolKey::from("i"))) .expect_instr( 2, - Instruction::LoadInteger(LoadISpan { name: SymbolKey::from("i"), pos: lc(1, 5) }), + 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))) @@ -2083,7 +2128,11 @@ mod tests { .expect_instr(7, Instruction::Assign(SymbolKey::from("j"))) .expect_instr( 8, - Instruction::LoadInteger(LoadISpan { name: SymbolKey::from("j"), pos: lc(2, 5) }), + 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))) @@ -2091,7 +2140,11 @@ mod tests { .expect_instr(12, Instruction::Jump(JumpISpan { addr: 18 })) // Exit for. .expect_instr( 13, - Instruction::LoadInteger(LoadISpan { name: SymbolKey::from("j"), pos: lc(2, 5) }), + 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))) @@ -2101,7 +2154,11 @@ mod tests { .expect_instr(18, Instruction::Jump(JumpISpan { addr: 24 })) // Exit for. .expect_instr( 19, - Instruction::LoadInteger(LoadISpan { name: SymbolKey::from("i"), pos: lc(1, 5) }), + 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))) @@ -2135,7 +2192,11 @@ mod tests { .expect_instr(1, Instruction::Assign(SymbolKey::from("i"))) .expect_instr( 2, - Instruction::LoadInteger(LoadISpan { name: SymbolKey::from("i"), pos: lc(2, 5) }), + 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))) @@ -2147,7 +2208,11 @@ mod tests { .expect_instr(8, Instruction::Jump(JumpISpan { addr: 14 })) // Exit for. .expect_instr( 9, - Instruction::LoadInteger(LoadISpan { name: SymbolKey::from("i"), pos: lc(2, 5) }), + 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))) @@ -2170,6 +2235,7 @@ mod tests { Instruction::LoadInteger(LoadISpan { name: SymbolKey::from("iter"), pos: lc(1, 5), + index: 0, }), ) .expect_instr(3, Instruction::PushInteger(5, lc(1, 17))) @@ -2182,6 +2248,7 @@ mod tests { Instruction::LoadInteger(LoadISpan { name: SymbolKey::from("iter"), pos: lc(1, 5), + index: 0, }), ) .expect_instr(9, Instruction::PushInteger(1, lc(1, 18))) @@ -2200,7 +2267,11 @@ mod tests { .compile() .expect_instr( 0, - Instruction::LoadInteger(LoadISpan { name: SymbolKey::from("i"), pos: lc(1, 12) }), + Instruction::LoadInteger(LoadISpan { + name: SymbolKey::from("i"), + pos: lc(1, 12), + index: 0, + }), ) .expect_instr(1, Instruction::Assign(SymbolKey::from("iter"))) .expect_instr( @@ -2208,11 +2279,16 @@ mod tests { 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) }), + 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)) @@ -2223,6 +2299,7 @@ mod tests { Instruction::LoadInteger(LoadISpan { name: SymbolKey::from("iter"), pos: lc(1, 5), + index: 2, }), ) .expect_instr(9, Instruction::PushInteger(1, lc(1, 18))) @@ -2241,7 +2318,11 @@ mod tests { .compile() .expect_instr( 0, - Instruction::LoadInteger(LoadISpan { name: SymbolKey::from("i"), pos: lc(1, 13) }), + 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))) @@ -2251,12 +2332,17 @@ mod tests { 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(LoadISpan { name: SymbolKey::from("j"), pos: lc(1, 28) }), + 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))) @@ -2268,6 +2354,7 @@ mod tests { Instruction::LoadInteger(LoadISpan { name: SymbolKey::from("iter"), pos: lc(1, 5), + index: 2, }), ) .expect_instr(13, Instruction::PushInteger(1, lc(1, 30))) @@ -2304,7 +2391,11 @@ mod tests { .expect_instr(4, Instruction::Assign(SymbolKey::from("iter"))) .expect_instr( 5, - Instruction::LoadDouble(LoadISpan { name: SymbolKey::from("iter"), pos: lc(1, 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) @@ -2312,7 +2403,11 @@ mod tests { .expect_instr(9, Instruction::JumpIfNotTrue(15)) .expect_instr( 10, - Instruction::LoadDouble(LoadISpan { name: SymbolKey::from("iter"), pos: lc(1, 5) }), + 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))) @@ -2344,6 +2439,7 @@ mod tests { Instruction::LoadInteger(LoadISpan { name: SymbolKey::from("0return_foo"), pos: lc(1, 22), + index: 0, }), ) .expect_instr(6, Instruction::LeaveScope) @@ -2384,6 +2480,7 @@ mod tests { Instruction::LoadInteger(LoadISpan { name: SymbolKey::from("0return_foo"), pos: lc(1, 27), + index: 0, }), ) .expect_instr(8, Instruction::LeaveScope) @@ -2425,6 +2522,7 @@ mod tests { Instruction::LoadInteger(LoadISpan { name: SymbolKey::from("0return_foo"), pos: lc(1, 44), + index: 0, }), ) .expect_instr(9, Instruction::LeaveScope) @@ -2738,6 +2836,7 @@ mod tests { 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)), @@ -2755,6 +2854,7 @@ mod tests { 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)), @@ -2769,6 +2869,7 @@ mod tests { Instruction::LoadInteger(LoadISpan { name: SymbolKey::from("0select1"), pos: lc(2, 12), + index: 0, }), Instruction::PushInteger(9, lc(2, 12)), Instruction::NotEqualIntegers(lc(2, 12)), @@ -2781,6 +2882,7 @@ mod tests { Instruction::LoadInteger(LoadISpan { name: SymbolKey::from("0select1"), pos: lc(2, 11), + index: 0, }), Instruction::PushInteger(9, lc(2, 11)), Instruction::LessIntegers(lc(2, 11)), @@ -2793,6 +2895,7 @@ mod tests { Instruction::LoadInteger(LoadISpan { name: SymbolKey::from("0select1"), pos: lc(2, 12), + index: 0, }), Instruction::PushInteger(9, lc(2, 12)), Instruction::LessEqualIntegers(lc(2, 12)), @@ -2805,6 +2908,7 @@ mod tests { Instruction::LoadInteger(LoadISpan { name: SymbolKey::from("0select1"), pos: lc(2, 11), + index: 0, }), Instruction::PushInteger(9, lc(2, 11)), Instruction::GreaterIntegers(lc(2, 11)), @@ -2817,6 +2921,7 @@ mod tests { Instruction::LoadInteger(LoadISpan { name: SymbolKey::from("0select1"), pos: lc(2, 12), + index: 0, }), Instruction::PushInteger(9, lc(2, 12)), Instruction::GreaterEqualIntegers(lc(2, 12)), @@ -2832,12 +2937,14 @@ mod tests { 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(LoadISpan { name: SymbolKey::from("0select1"), pos: lc(2, 6), + index: 0, }), Instruction::PushInteger(2, lc(2, 11)), Instruction::LessEqualIntegers(lc(2, 11)), @@ -2854,12 +2961,14 @@ mod tests { 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(LoadISpan { name: SymbolKey::from("0select1"), pos: lc(2, 15), + index: 0, }), Instruction::PushInteger(8, lc(2, 15)), Instruction::EqualIntegers(lc(2, 15)), @@ -2900,6 +3009,7 @@ mod tests { Instruction::LoadInteger(LoadISpan { name: SymbolKey::from("0select1"), pos: lc(2, 6), + index: 0, }), ) .expect_instr(3, Instruction::PushInteger(7, lc(2, 6))) @@ -2929,7 +3039,11 @@ mod tests { .compile() .expect_instr( 0, - Instruction::LoadInteger(LoadISpan { name: SymbolKey::from("i"), pos: lc(1, 13) }), + Instruction::LoadInteger(LoadISpan { + name: SymbolKey::from("i"), + pos: lc(1, 13), + index: 0, + }), ) .expect_instr(1, Instruction::Assign(SymbolKey::from("0select1"))) .expect_instr( @@ -2980,6 +3094,7 @@ mod tests { Instruction::LoadInteger(LoadISpan { name: SymbolKey::from("0select1"), pos: lc(2, 6), + index: 0, }), ) .expect_instr(3, Instruction::PushInteger(7, lc(2, 6))) @@ -3000,6 +3115,7 @@ mod tests { Instruction::LoadInteger(LoadISpan { name: SymbolKey::from("0select1"), pos: lc(4, 12), + index: 0, }), ) .expect_instr(9, Instruction::PushInteger(8, lc(4, 12))) @@ -3035,6 +3151,7 @@ mod tests { Instruction::LoadInteger(LoadISpan { name: SymbolKey::from("0select1"), pos: lc(2, 6), + index: 0, }), ) .expect_instr(3, Instruction::PushInteger(7, lc(2, 6))) diff --git a/core/src/compiler/symtable.rs b/core/src/compiler/symtable.rs index 90c3d874..8833064b 100644 --- a/core/src/compiler/symtable.rs +++ b/core/src/compiler/symtable.rs @@ -89,6 +89,12 @@ impl> IndexedHashMap { 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); @@ -293,6 +299,11 @@ impl SymbolsTable { 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); From 70334cd6ee558fa102f9dbaafcd72a1002a31783 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Sun, 11 Jan 2026 07:32:20 -0800 Subject: [PATCH 12/13] Assign indexes to unset operations --- core/src/bytecode.rs | 3 ++ core/src/compiler/mod.rs | 55 ++++++++++++++++++++++++++--------- core/src/compiler/symtable.rs | 2 ++ 3 files changed, 47 insertions(+), 13 deletions(-) diff --git a/core/src/bytecode.rs b/core/src/bytecode.rs index 497dda1f..e24636c4 100644 --- a/core/src/bytecode.rs +++ b/core/src/bytecode.rs @@ -177,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. diff --git a/core/src/compiler/mod.rs b/core/src/compiler/mod.rs index a5f69884..35208bb0 100644 --- a/core/src/compiler/mod.rs +++ b/core/src/compiler/mod.rs @@ -311,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)?; @@ -321,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))) } }; @@ -347,7 +348,7 @@ impl Compiler { self.emit(Instruction::Assign(key)); - Ok(()) + Ok(index) } /// Compiles a `FUNCTION` or `SUB` definition. @@ -634,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(); @@ -667,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(()) @@ -2823,7 +2828,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(); } @@ -2991,6 +3000,7 @@ mod tests { Instruction::Unset(UnsetISpan { name: SymbolKey::from("0select1"), pos: lc(1, 20), + index: 0, }), ) .check(); @@ -3026,7 +3036,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(); } @@ -3051,6 +3065,7 @@ mod tests { Instruction::Unset(UnsetISpan { name: SymbolKey::from("0select1"), pos: lc(1, 16), + index: 1, }), ) .check(); @@ -3075,7 +3090,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(); } @@ -3132,7 +3151,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(); } @@ -3178,7 +3201,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(); } @@ -3195,6 +3222,7 @@ mod tests { Instruction::Unset(UnsetISpan { name: SymbolKey::from("0select1"), pos: lc(1, 16), + index: 0, }), ) .expect_instr(3, Instruction::PushInteger(0, lc(2, 13))) @@ -3204,6 +3232,7 @@ mod tests { 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 index 8833064b..2e90827d 100644 --- a/core/src/compiler/symtable.rs +++ b/core/src/compiler/symtable.rs @@ -110,6 +110,7 @@ impl> IndexedHashMap { } /// Same as `HashMap::get`. + #[cfg(test)] fn get(&self, key: &K) -> Option<&V> { self.map.get(key).map(|v| &v.0) } @@ -258,6 +259,7 @@ impl SymbolsTable { } /// 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() { From 67e1892b7494740914a26e513001c52cbcddb504 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Sun, 11 Jan 2026 07:34:36 -0800 Subject: [PATCH 13/13] Assign indexes to assignments This finishes modifying the bytecode so that all operations that reference symbols carry the corresponding stack index with them. --- core/src/bytecode.rs | 8 ++-- core/src/compiler/exprs.rs | 34 +++++++------- core/src/compiler/mod.rs | 90 +++++++++++++++++++------------------- core/src/exec.rs | 2 +- 4 files changed, 68 insertions(+), 66 deletions(-) diff --git a/core/src/bytecode.rs b/core/src/bytecode.rs index e24636c4..7512ea01 100644 --- a/core/src/bytecode.rs +++ b/core/src/bytecode.rs @@ -327,7 +327,7 @@ pub enum Instruction { 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), @@ -479,7 +479,7 @@ impl Instruction { ("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))) @@ -629,7 +629,7 @@ impl Instruction { Instruction::ArrayAssignment(span) => Some(span.name_pos), Instruction::ArrayLoad(span) => Some(span.name_pos), - Instruction::Assign(_) => None, + Instruction::Assign(..) => None, Instruction::BuiltinCall(span) => Some(span.name_pos), Instruction::Call(_) => None, Instruction::FunctionCall(span) => Some(span.name_pos), @@ -729,7 +729,7 @@ impl Instruction { | Instruction::LeaveScope => false, Instruction::ArrayAssignment(..) - | Instruction::Assign(_) + | Instruction::Assign(..) | Instruction::BuiltinCall(_) | Instruction::Call(_) | Instruction::Dim(_) diff --git a/core/src/compiler/exprs.rs b/core/src/compiler/exprs.rs index 9a567705..6681b12c 100644 --- a/core/src/compiler/exprs.rs +++ b/core/src/compiler/exprs.rs @@ -834,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(); } @@ -858,7 +858,7 @@ mod tests { index: 0, }), ) - .expect_instr(1, Instruction::Assign(SymbolKey::from("i"))) + .expect_instr(1, Instruction::Assign(SymbolKey::from("i"), 1)) .check(); } @@ -878,7 +878,7 @@ mod tests { nargs: 0, }), ) - .expect_instr(1, Instruction::Assign(SymbolKey::from("i"))) + .expect_instr(1, Instruction::Assign(SymbolKey::from("i"), 0)) .check(); } @@ -915,7 +915,7 @@ mod tests { nargs: 0, }), ) - .expect_instr(1, Instruction::Assign(SymbolKey::from("i"))) + .expect_instr(1, Instruction::Assign(SymbolKey::from("i"), 0)) .expect_instr(2, Instruction::PushInteger(3, lc(2, 7))) .expect_instr( 3, @@ -927,7 +927,7 @@ mod tests { nargs: 1, }), ) - .expect_instr(4, Instruction::Assign(SymbolKey::from("j"))) + .expect_instr(4, Instruction::Assign(SymbolKey::from("j"), 1)) .check(); } @@ -1148,7 +1148,7 @@ mod tests { ) .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(); } @@ -1178,7 +1178,7 @@ mod tests { }), ) .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(); } @@ -1217,7 +1217,7 @@ mod tests { ) .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(); } @@ -1301,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); @@ -1316,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); @@ -1358,7 +1358,7 @@ mod tests { nargs: 3, }), ) - .expect_instr(6, Instruction::Assign(SymbolKey::from("i"))) + .expect_instr(6, Instruction::Assign(SymbolKey::from("i"), 3)) .check(); } @@ -1378,7 +1378,7 @@ mod tests { nargs: 1, }), ) - .expect_instr(2, Instruction::Assign(SymbolKey::from("i"))) + .expect_instr(2, Instruction::Assign(SymbolKey::from("i"), 1)) .check(); } @@ -1409,7 +1409,7 @@ mod tests { nargs: 1, }), ) - .expect_instr(3, Instruction::Assign(SymbolKey::from("i"))) + .expect_instr(3, Instruction::Assign(SymbolKey::from("i"), 1)) .check(); } @@ -1500,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 35208bb0..0b414cfe 100644 --- a/core/src/compiler/mod.rs +++ b/core/src/compiler/mod.rs @@ -346,7 +346,7 @@ impl Compiler { } } - self.emit(Instruction::Assign(key)); + self.emit(Instruction::Assign(key, index)); Ok(index) } @@ -971,8 +971,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"); @@ -1009,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"); @@ -1485,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(); } @@ -1503,7 +1505,7 @@ mod tests { index: 0, }), ) - .expect_instr(1, Instruction::Assign(SymbolKey::from("foo"))) + .expect_instr(1, Instruction::Assign(SymbolKey::from("foo"), 1)) .check(); } @@ -1513,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(); } @@ -1525,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(); } @@ -2082,7 +2084,7 @@ 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(1, Instruction::Assign(SymbolKey::from("i"), 0)) .expect_instr( 2, Instruction::LoadInteger(LoadISpan { @@ -2105,7 +2107,7 @@ mod tests { ) .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(); } @@ -2116,7 +2118,7 @@ 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(1, Instruction::Assign(SymbolKey::from("i"), 0)) .expect_instr( 2, Instruction::LoadInteger(LoadISpan { @@ -2130,7 +2132,7 @@ mod tests { .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(7, Instruction::Assign(SymbolKey::from("j"), 1)) .expect_instr( 8, Instruction::LoadInteger(LoadISpan { @@ -2153,7 +2155,7 @@ mod tests { ) .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. @@ -2167,7 +2169,7 @@ mod tests { ) .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(); } @@ -2194,7 +2196,7 @@ 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(1, Instruction::Assign(SymbolKey::from("i"), 0)) .expect_instr( 2, Instruction::LoadInteger(LoadISpan { @@ -2221,7 +2223,7 @@ mod tests { ) .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 })) @@ -2234,7 +2236,7 @@ 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(1, Instruction::Assign(SymbolKey::from("iter"), 0)) .expect_instr( 2, Instruction::LoadInteger(LoadISpan { @@ -2247,7 +2249,7 @@ mod tests { .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(7, Instruction::Assign(SymbolKey::from("a"), 1)) .expect_instr( 8, Instruction::LoadInteger(LoadISpan { @@ -2258,7 +2260,7 @@ mod tests { ) .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(); } @@ -2278,7 +2280,7 @@ mod tests { index: 0, }), ) - .expect_instr(1, Instruction::Assign(SymbolKey::from("iter"))) + .expect_instr(1, Instruction::Assign(SymbolKey::from("iter"), 2)) .expect_instr( 2, Instruction::LoadInteger(LoadISpan { @@ -2298,7 +2300,7 @@ mod tests { .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(7, Instruction::Assign(SymbolKey::from("a"), 3)) .expect_instr( 8, Instruction::LoadInteger(LoadISpan { @@ -2309,7 +2311,7 @@ mod tests { ) .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(); } @@ -2331,7 +2333,7 @@ mod tests { ) .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(3, Instruction::Assign(SymbolKey::from("iter"), 2)) .expect_instr( 4, Instruction::LoadInteger(LoadISpan { @@ -2353,7 +2355,7 @@ mod tests { .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(11, Instruction::Assign(SymbolKey::from("a"), 3)) .expect_instr( 12, Instruction::LoadInteger(LoadISpan { @@ -2364,7 +2366,7 @@ mod tests { ) .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(); } @@ -2393,7 +2395,7 @@ mod tests { ) .expect_instr(2, Instruction::PushInteger(0, lc(1, 12))) .expect_instr(3, Instruction::IntegerToDouble) - .expect_instr(4, Instruction::Assign(SymbolKey::from("iter"))) + .expect_instr(4, Instruction::Assign(SymbolKey::from("iter"), 0)) .expect_instr( 5, Instruction::LoadDouble(LoadISpan { @@ -2416,7 +2418,7 @@ mod tests { ) .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(); } @@ -2438,7 +2440,7 @@ mod tests { }), ) .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::LoadInteger(LoadISpan { @@ -2466,9 +2468,9 @@ 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( @@ -2518,10 +2520,10 @@ mod tests { }), ) .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(7, Instruction::Assign(SymbolKey::from("a"), 1)) .expect_instr( 8, Instruction::LoadInteger(LoadISpan { @@ -2596,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( @@ -2810,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); @@ -2994,7 +2996,7 @@ 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 { @@ -3013,7 +3015,7 @@ 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(1, Instruction::Assign(SymbolKey::from("0select1"), 0)) .expect_instr( 2, Instruction::LoadInteger(LoadISpan { @@ -3059,7 +3061,7 @@ mod tests { index: 0, }), ) - .expect_instr(1, Instruction::Assign(SymbolKey::from("0select1"))) + .expect_instr(1, Instruction::Assign(SymbolKey::from("0select1"), 1)) .expect_instr( 2, Instruction::Unset(UnsetISpan { @@ -3078,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 { @@ -3107,7 +3109,7 @@ 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(1, Instruction::Assign(SymbolKey::from("0select1"), 0)) .expect_instr( 2, Instruction::LoadInteger(LoadISpan { @@ -3168,7 +3170,7 @@ 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(1, Instruction::Assign(SymbolKey::from("0select1"), 0)) .expect_instr( 2, Instruction::LoadInteger(LoadISpan { @@ -3216,7 +3218,7 @@ 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 { @@ -3226,7 +3228,7 @@ mod tests { }), ) .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 { diff --git a/core/src/exec.rs b/core/src/exec.rs index 8158cdaf..36b1cea0 100644 --- a/core/src/exec.rs +++ b/core/src/exec.rs @@ -1185,7 +1185,7 @@ 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;